├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── docker-publish.yml │ └── tmkms.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile.nitro ├── Dockerfile.sgx ├── LICENSE ├── Logos ├── C Crypto.com (green).png ├── Lionhead Crypto.com (blue).png └── README.md ├── NOTICE ├── README.md ├── SECURITY.md ├── integration-test ├── default.nix ├── flake.lock ├── flake.nix ├── integration_test │ ├── __init__.py │ └── bot.py ├── nix │ ├── default.nix │ ├── sources.json │ └── sources.nix ├── poetry.lock ├── pyproject.toml ├── run.sh └── tests │ ├── __init__.py │ └── test_integration_test.py ├── providers ├── nitro │ ├── nitro-enclave │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── main.rs │ │ │ ├── nitro.rs │ │ │ └── nitro │ │ │ └── state.rs │ └── nitro-helper │ │ ├── Cargo.toml │ │ └── src │ │ ├── command.rs │ │ ├── command │ │ ├── launch_all.rs │ │ └── nitro_enclave.rs │ │ ├── config.rs │ │ ├── enclave_log_server.rs │ │ ├── key_utils.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── proxy.rs │ │ ├── shared.rs │ │ ├── state.rs │ │ └── tracing_layer.rs ├── sgx │ ├── sgx-app │ │ ├── .cargo │ │ │ └── config │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── main.rs │ │ │ ├── sgx_app.rs │ │ │ └── sgx_app │ │ │ ├── cloud.rs │ │ │ ├── cloud │ │ │ └── keywrap.rs │ │ │ ├── keypair_seal.rs │ │ │ └── state.rs │ └── sgx-runner │ │ ├── Cargo.toml │ │ └── src │ │ ├── command.rs │ │ ├── config.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── runner.rs │ │ ├── shared.rs │ │ └── state.rs └── softsign │ ├── Cargo.toml │ └── src │ ├── config.rs │ ├── key_utils.rs │ ├── main.rs │ └── state.rs ├── script └── tmkms-nitro │ ├── default.nix │ └── verify.py └── src ├── chain.rs ├── chain ├── state.rs └── state │ └── error.rs ├── config.rs ├── config └── validator.rs ├── connection.rs ├── error.rs ├── lib.rs ├── rpc.rs ├── session.rs └── utils.rs /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.217.1/containers/rust/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye 4 | ARG VARIANT="buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT} 6 | RUN rustup default nightly-2023-02-20 && rustup target add x86_64-fortanix-unknown-sgx && rustup target add x86_64-fortanix-unknown-sgx && rustup target add x86_64-unknown-linux-musl && rustup component add rust-src rustfmt clippy 7 | # [Optional] Uncomment this section to install additional packages. 8 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | && apt-get -y install --no-install-recommends protobuf-compiler 10 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.217.1/containers/rust 3 | { 4 | "name": "Rust", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Use the VARIANT arg to pick a Debian OS version: buster, bullseye 9 | // Use bullseye when on local on arm64/Apple Silicon. 10 | "VARIANT": "buster" 11 | } 12 | }, 13 | "runArgs": [ 14 | "--cap-add=SYS_PTRACE", 15 | "--security-opt", 16 | "seccomp=unconfined" 17 | ], 18 | 19 | // Set *default* container specific settings.json values on container create. 20 | "settings": { 21 | "lldb.executable": "/usr/bin/lldb", 22 | // VS Code don't watch files under ./target 23 | "files.watcherExclude": { 24 | "**/target/**": true 25 | }, 26 | "rust-analyzer.checkOnSave.command": "clippy" 27 | }, 28 | 29 | // Add the IDs of extensions you want installed when the container is created. 30 | "extensions": [ 31 | "vadimcn.vscode-lldb", 32 | "mutantdino.resourcemonitor", 33 | "matklad.rust-analyzer", 34 | "tamasfe.even-better-toml", 35 | "serayuzgur.crates" 36 | ], 37 | 38 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 39 | // "forwardPorts": [], 40 | 41 | // Use 'postCreateCommand' to run commands after the container is created. 42 | // "postCreateCommand": "rustc --version", 43 | 44 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 45 | "remoteUser": "vscode", 46 | "features": { 47 | "git": "os-provided" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !providers 3 | !src 4 | !Cargo.lock 5 | !Cargo.toml -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @crypto-com/tmkms-maintainers 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 👮🏻👮🏻👮🏻 !!!! REFERENCE THE PROBLEM YOUR ARE SOLVING IN THE PR TITLE AND DESCRIBE YOUR SOLUTION HERE !!!! DO NOT FORGET !!!! 👮🏻👮🏻👮🏻 2 | 3 | 4 | # PR Checklist: 5 | 6 | - [ ] Have you read the [CONTRIBUTING.md](https://github.com/crypto-com/tmkms-light/blob/main/CONTRIBUTING.md)? 7 | - [ ] Does your PR follow the [C4 patch requirements](https://rfc.zeromq.org/spec:42/C4/#23-patch-requirements)? 8 | - [ ] Have you rebased your work on top of the latest main? 9 | - [ ] Have you checked your code compiles? (`cargo build`) 10 | - [ ] Have you included tests for any non-trivial functionality? 11 | - [ ] Have you checked your code passes the unit tests? (`cargo test`) 12 | - [ ] Have you checked your code formatting is correct? (`cargo fmt -- --check --color=auto`) 13 | - [ ] Have you checked your basic code style is fine? (`cargo clippy`) 14 | - [ ] If you added any dependencies, have you checked they do not contain any known vulnerabilities? (`cargo audit`) 15 | - [ ] If your changes affect public APIs, does your PR follow the [C4 evolution of public contracts](https://rfc.zeromq.org/spec:42/C4/#26-evolution-of-public-contracts)? 16 | - [ ] If your code changes public APIs, have you incremented the crate version numbers and documented your changes in the [CHANGELOG.md](https://github.com/crypto-com/tmkms-light/blob/main/CHANGELOG.md)? 17 | - [ ] If you are contributing for the first time, please read the agreement in [CONTRIBUTING.md](https://github.com/crypto-com/tmkms-light/blob/main/CONTRIBUTING.md) now and add a comment to this pull request stating that your PR is in accordance with the [Developer's Certificate of Origin](https://github.com/crypto-com/tmkms-light/blob/main/CONTRIBUTING.md#developer-certificate-of-originn). 18 | 19 | Thank you for your code, it's appreciated! :) 20 | 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: docker-publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | environment: dockerhub 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Get the version 15 | id: get_version 16 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v2 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v2 21 | - name: Login to DockerHub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKERHUB_USERNAME }} 25 | password: ${{ secrets.DOCKERHUB_TOKEN }} 26 | - name: Build and push 27 | uses: docker/build-push-action@v3 28 | with: 29 | context: . 30 | push: true 31 | platforms: linux/amd64,linux/arm64 32 | file: Dockerfile.nitro 33 | build-args: | 34 | RUST_TOOLCHAIN=1.66.1 35 | tags: | 36 | cryptocom/nitro-enclave-tmkms:latest 37 | cryptocom/nitro-enclave-tmkms:${{ steps.get_version.outputs.VERSION }} 38 | -------------------------------------------------------------------------------- /.github/workflows/tmkms.yml: -------------------------------------------------------------------------------- 1 | name: tmkms 2 | 3 | on: 4 | push: 5 | branches: main 6 | merge_group: 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | RUSTFLAGS: -Dwarnings 14 | 15 | jobs: 16 | rustfmt: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: rustfmt 23 | - run: cargo fmt --all -- --check 24 | 25 | clippy: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Install deps 30 | run: sudo apt-get update && sudo apt-get install protobuf-compiler 31 | - uses: dtolnay/rust-toolchain@master 32 | with: 33 | components: clippy 34 | toolchain: nightly-2023-02-20 35 | - run: cargo clippy --all -- -D warnings 36 | 37 | audit: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: dtolnay/rust-toolchain@master 42 | with: 43 | toolchain: nightly-2023-02-20 44 | - name: Install cargo audit 45 | run: cargo install cargo-audit 46 | # TODO: unmaintained dependencies -- need to be fixed in upstream 47 | # RUSTSEC-2019-0036,RUSTSEC-2020-0036: failure crate used in upstream sgxs-* crates 48 | # RUSTSEC-2020-0071: time crate used in upstream sgxs-* crates 49 | # RUSTSEC-2021-0127: serde_cbor used in upstream nsm-* crates 50 | # RUSTSEC-2020-0016,RUSTSEC-2021-0124: net2 used in mio from older tokio (used in sgx crates) 51 | # RUSTSEC-2022-0041: old cross-beam used in the sgx runner crate 52 | # RUSTSEC-2023-0005: old tokio used in the sgx runner crate (enclave-runner) 53 | # RUSTSEC-2023-0045: old cross-beam used in the sgx runner crate 54 | - run: > 55 | cargo audit --deny warnings 56 | --ignore RUSTSEC-2020-0071 57 | --ignore RUSTSEC-2021-0124 58 | --ignore RUSTSEC-2020-0036 59 | --ignore RUSTSEC-2020-0016 60 | --ignore RUSTSEC-2021-0127 61 | --ignore RUSTSEC-2019-0036 62 | --ignore RUSTSEC-2022-0041 63 | --ignore RUSTSEC-2023-0005 64 | --ignore RUSTSEC-2023-0045 65 | 66 | build: 67 | runs-on: ubuntu-latest 68 | strategy: 69 | matrix: 70 | crate: 71 | - tmkms-nitro-helper 72 | - tmkms-light-sgx-runner 73 | - tmkms-softsign 74 | steps: 75 | - uses: actions/checkout@v3 76 | - name: Install deps 77 | run: sudo apt-get update && sudo apt-get install protobuf-compiler 78 | - uses: dtolnay/rust-toolchain@master 79 | with: 80 | targets: x86_64-unknown-linux-gnu 81 | toolchain: nightly-2023-02-20 82 | - run: cargo build --target x86_64-unknown-linux-gnu -p ${{ matrix.crate }} --release 83 | - name: 'Tar files' 84 | run: cd target/x86_64-unknown-linux-gnu/release/ && tar -cvf tmkms-softsign.tar tmkms-softsign 85 | if: startsWith(matrix.crate, 'tmkms-softsign') 86 | - uses: actions/upload-artifact@v2 87 | if: startsWith(matrix.crate, 'tmkms-softsign') 88 | with: 89 | name: tmkms-softsign 90 | path: target/x86_64-unknown-linux-gnu/release/tmkms-softsign.tar 91 | build-sgx: 92 | runs-on: ubuntu-latest 93 | steps: 94 | - uses: actions/checkout@v3 95 | - uses: dtolnay/rust-toolchain@master 96 | with: 97 | targets: x86_64-fortanix-unknown-sgx 98 | toolchain: nightly-2023-02-20 99 | - run: cargo build --target x86_64-fortanix-unknown-sgx -p tmkms-light-sgx-app --release 100 | build-nitro: 101 | runs-on: ubuntu-latest 102 | steps: 103 | - uses: actions/checkout@v3 104 | - name: Set up QEMU 105 | uses: docker/setup-qemu-action@v2 106 | - name: Set up Docker Buildx 107 | uses: docker/setup-buildx-action@v2 108 | - name: Build tmkms-nitro-enclave with docker 109 | uses: docker/build-push-action@v3 110 | with: 111 | context: . 112 | push: false 113 | platforms: linux/amd64,linux/arm64 114 | file: Dockerfile.nitro 115 | build-args: | 116 | RUST_TOOLCHAIN=1.66.1 117 | run-integration-tests: 118 | runs-on: ubuntu-latest 119 | needs: build 120 | steps: 121 | - uses: actions/checkout@v3 122 | - uses: cachix/install-nix-action@v19 123 | with: 124 | # pin to nix-2.13 to workaround compability issue of 2.14, 125 | # see: https://github.com/cachix/install-nix-action/issues/161 126 | install_url: https://releases.nixos.org/nix/nix-2.13.3/install 127 | - uses: cachix/cachix-action@v12 128 | with: 129 | name: crypto-com 130 | skipPush: true 131 | - uses: actions/download-artifact@v2 132 | with: 133 | name: tmkms-softsign 134 | path: integration-test 135 | - run: cd integration-test && tar -xvf tmkms-softsign.tar && chmod +x tmkms-softsign && nix develop --extra-experimental-features nix-command --extra-experimental-features flakes -c ./run.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | *January 5, 2023* 2 | 3 | Many dependency upgrades, support for multi-arch Nitro images, and a small bug fix. 4 | 5 | ## v0.4.2 6 | ### Improvements 7 | * [486](https://github.com/crypto-com/tmkms-light/pull/486) arm64 support in the Nitro image 8 | ### Bug Fixes 9 | * [409](https://github.com/crypto-com/tmkms-light/pull/409) possible panics fixed in the Nitro logging 10 | 11 | *August 9, 2022* 12 | 13 | A bug fix for the SGX provider and a few dependency upgrades. 14 | 15 | ## v0.4.1 16 | ### Bug Fixes 17 | * [387](https://github.com/crypto-com/tmkms-light/pull/387) SGX initialization and communication fix 18 | 19 | *August 4, 2022* 20 | 21 | Many dependency upgrades. 22 | 23 | ## v0.4.0 24 | ### Breaking changes 25 | * [356](https://github.com/crypto-com/tmkms-light/pull/356) NE production enclave file logging removed 26 | in order to remove a vulnerable crate (you can use the default console logging and e.g. the Journald service instead) 27 | 28 | ### Improvements 29 | * [288](https://github.com/crypto-com/tmkms-light/pull/288) switch to the official AWS Rust SDK 30 | * [286](https://github.com/crypto-com/tmkms-light/pull/286) switch from the deprecated error-reporting crate 31 | * [308](https://github.com/crypto-com/tmkms-light/pull/308) NE init command check if vsock-proxy is running 32 | * [342](https://github.com/crypto-com/tmkms-light/pull/342) NE launch-all consistent exit code for errors 33 | 34 | ### Bug Fixes 35 | * [305](https://github.com/crypto-com/tmkms-light/pull/305) NE init fixed 36 | 37 | *October 29, 2021* 38 | 39 | Many dependency upgrades and NE improvements. 40 | Note that the version 0.2.0 contained a bug where production logging information from NE was sometimes lost. 41 | 42 | ## v0.3.0 43 | ### Breaking changes 44 | * [109](https://github.com/crypto-com/tmkms-light/pull/109) "launch-all" command in the NE helper 45 | 46 | ### Improvements 47 | * [115](https://github.com/crypto-com/tmkms-light/pull/115) graceful process shutdown in NE 48 | 49 | ### Bug Fixes 50 | * [109](https://github.com/crypto-com/tmkms-light/pull/109) production logging in NE fix 51 | 52 | 53 | *June 25, 2021* 54 | 55 | Dependency upgrades, logging and backup/recovery improvements. 56 | ## v0.2.0 57 | ### Breaking changes 58 | * [68](https://github.com/crypto-com/tmkms-light/pull/68) production logging in NE 59 | * [87](https://github.com/crypto-com/tmkms-light/pull/87) keygen in NE 60 | * [95](https://github.com/crypto-com/tmkms-light/pull/95) attestation for keygen in NE 61 | * [94](https://github.com/crypto-com/tmkms-light/pull/94) logging level configurable in enclaves and hosts 62 | * [55](https://github.com/crypto-com/tmkms-light/pull/55) SGX init and recovery commands paramater change 63 | * [80](https://github.com/crypto-com/tmkms-light/pull/80) SGX backup and recovery in cloud environments 64 | 65 | ### Improvements 66 | * [89](https://github.com/crypto-com/tmkms-light/pull/89) Instance Metadata Service Version 2 used in the NE host 67 | 68 | *March 18, 2021* 69 | 70 | Tendermint-rs and prost dependencies upgraded. 71 | ## v0.1.2 72 | 73 | *March 16, 2021* 74 | 75 | The same as the initial released version, but with a few dependency upgrades 76 | ## v0.1.1 77 | 78 | *March 2, 2021* 79 | The initial released version 80 | ## v0.1.0 81 | ### Breaking changes 82 | ### Features 83 | ### Improvements 84 | ### Bug Fixes 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Conduct 4 | ### Contact: chain@crypto.com 5 | 6 | * We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic. 7 | 8 | * On communication channels, please avoid using overtly sexual nicknames or other nicknames that might detract from a friendly, safe and welcoming environment for all. 9 | 10 | * Please be kind and courteous. There’s no need to be mean or rude. 11 | 12 | * Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer. 13 | 14 | * Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works. 15 | 16 | * We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behaviour. We interpret the term “harassment” as including the definition in the [Citizen Code of Conduct](http://citizencodeofconduct.org/); if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don’t tolerate behavior that excludes people in socially marginalized groups. 17 | 18 | * Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the communication channel admins or the email mentioned above immediately. Whether you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve got your back. 19 | 20 | * Likewise any spamming, trolling, flaming, baiting or other attention-stealing behaviour is not welcome. 21 | 22 | 23 | ---- 24 | 25 | 26 | ## Moderation 27 | These are the policies for upholding our community’s standards of conduct. If you feel that a thread needs moderation, please contact the above mentioned person. 28 | 29 | 1. Remarks that violate the Crypto.com standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner.) 30 | 31 | 2. Remarks that moderators find inappropriate, whether listed in the code of conduct or not, are also not allowed. 32 | 33 | 3. Moderators will first respond to such remarks with a warning. 34 | 35 | 4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off. 36 | 37 | 5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded. 38 | 39 | 6. Moderators may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology. 40 | 41 | 7. If a moderator bans someone and you think it was unjustified, please take it up with that moderator, or with a different moderator, in private. Complaints about bans in-channel are not allowed. 42 | 43 | 8. Moderators are held to a higher standard than other community members. If a moderator creates an inappropriate situation, they should expect less leeway than others. 44 | 45 | In the Crypto.com developer community we strive to go the extra step to look out for each other. Don’t just aim to be technically unimpeachable, try to be your best self. In particular, avoid flirting with offensive or sensitive issues, particularly if they’re off-topic; this all too often leads to unnecessary fights, hurt feelings, and damaged trust; worse, it can drive people away from the community entirely. 46 | 47 | And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could’ve communicated better — remember that it’s your responsibility to make your fellow Crypto.com developer community members comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust. 48 | 49 | The enforcement policies listed above apply to all official Crypto.com venues. For other projects adopting the Crypto.com Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion. 50 | 51 | * Adapted from the the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html), the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](http://contributor-covenant.org/version/1/3/0/). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to Tendermint KMS Light! Good places to start are this document and [the original Tendermint KMS repository](https://github.com/iqlusioninc/tmkms). If you have any questions, feel free to ask on [Discord](https://discord.gg/pahqHz26q4). 4 | 5 | 6 | ## Code of Conduct 7 | 8 | All contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). 9 | 10 | ## Feature requests and bug reports 11 | 12 | Feature requests and bug reports should be posted as [Github issues](issues/new). 13 | In an issue, please describe what you did, what you expected, and what happened instead. 14 | 15 | If you think that you have identified an issue with Tendermint KMS Light that might compromise 16 | its users' security, please do not open a public issue on GitHub. Instead, 17 | we ask you to refer to [security policy](SECURITY.md). 18 | 19 | ## Working on issues 20 | There are several ways to identify an area where you can contribute to Tendermint KMS Light: 21 | 22 | * You can reach out by sending a message in the developer community communication channel, either with a specific contribution in mind or in general by saying "I want to help!". 23 | * Occasionally, some issues on Github may be labelled with `help wanted` or `good first issue` tags. 24 | 25 | We use the variation of the "fork and pull" model where contributors push changes to their personal fork and create pull requests to bring those changes into the source repository. 26 | Changes in pull requests should satisfy "Patch Requirements" described in [The Collective Code Construction Contract (C4)](https://rfc.zeromq.org/spec:42/C4/#23-patch-requirements). The code should follow [Rust Style Guide](https://github.com/rust-lang/rfcs/tree/master/style-guide). Many of the code style rules are captured by [rustfmt](https://github.com/rust-lang/rustfmt), so please make sure to use `cargo fmt` before every commit (e.g. by configuring your editor to do it for you upon saving a file). The code comments should follow [Rust API Documentation guidelines and conventions](https://rust-lang-nursery.github.io/api-guidelines/documentation.html). 27 | 28 | Once you identified an issue to work on, this is the summary of your basic steps: 29 | 30 | * Fork Tendermint KMS Light's repository under your Github account. 31 | 32 | * Clone your fork locally on your machine. 33 | 34 | * Post a comment in the issue to say that you are working on it, so that other people do not work on the same issue. 35 | 36 | * Create a local branch on your machine by `git checkout -b branch_name`. 37 | 38 | * Commit your changes to your own fork -- see [C4 Patch Requirements](https://rfc.zeromq.org/spec:42/C4/#23-patch-requirements) for guidelines. 39 | 40 | * Include tests that cover all non-trivial code. 41 | 42 | * Check you are working on the latest version on main in Tendermint KMS Light's official repository. If not, please pull Tendermint KMS Light's official repository's main (upstream) into your fork's main branch, and rebase your committed changes or replay your stashed changes in your branch over the latest changes in the upstream version. 43 | 44 | * Run all tests locally and make sure they pass. 45 | 46 | * If your changes are of interest to other developers, please make corresponding changes in the official documentation and the changelog. 47 | 48 | * Push your changes to your fork's branch and open the pull request to Tendermint KMS Light's repository main branch. 49 | 50 | * In the pull request, complete its checklist, add a clear description of the problem your changes solve, and add the following statement to confirm that your contribution is your own original work: "I hereby certify that my contribution is in accordance with the Developer Certificate of Origin (https://developercertificate.org/)." 51 | 52 | * The reviewer will either accept and merge your pull request, or leave comments requesting changes via the Github PR interface (you should then make changes by pushing directly to your existing PR branch). 53 | 54 | ### Developer Certificate of Origin 55 | All contributions to this project are subject to the terms of the Developer Certificate of Origin, available [here](https://developercertificate.org/) and reproduced below: 56 | 57 | ``` 58 | Developer Certificate of Origin 59 | Version 1.1 60 | 61 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 62 | 1 Letterman Drive 63 | Suite D4700 64 | San Francisco, CA, 94129 65 | 66 | Everyone is permitted to copy and distribute verbatim copies of this 67 | license document, but changing it is not allowed. 68 | 69 | Developer's Certificate of Origin 1.1 70 | 71 | By making a contribution to this project, I certify that: 72 | 73 | (a) The contribution was created in whole or in part by me and I 74 | have the right to submit it under the open source license 75 | indicated in the file; or 76 | 77 | (b) The contribution is based upon previous work that, to the best 78 | of my knowledge, is covered under an appropriate open source 79 | license and I have the right under that license to submit that 80 | work with modifications, whether created in whole or in part 81 | by me, under the same open source license (unless I am 82 | permitted to submit under a different license), as indicated 83 | in the file; or 84 | 85 | (c) The contribution was provided directly to me by some other 86 | person who certified (a), (b) or (c) and I have not modified 87 | it. 88 | 89 | (d) I understand and agree that this project and the contribution 90 | are public and that a record of the contribution (including all 91 | personal information I submit with it, including my sign-off) is 92 | maintained indefinitely and may be redistributed consistent with 93 | this project or the open source license(s) involved. 94 | ``` -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmkms-light" 3 | version = "0.4.2" 4 | authors = ["Tomas Tauber <2410580+tomtau@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | ed25519-consensus = "2" 11 | flex-error = "0.4" 12 | prost = "0.11" 13 | serde = { version = "1", features = ["serde_derive"] } 14 | serde_json = "1" 15 | subtle-encoding = { version = "0.5", features = ["bech32-preview"] } 16 | tendermint = "0.30" 17 | tendermint-proto = "0.30" 18 | tendermint-p2p = "0.30" 19 | tracing = "0.1" 20 | 21 | [workspace] 22 | members = ["providers/softsign", "providers/sgx/sgx-app", "providers/sgx/sgx-runner", "providers/nitro/nitro-enclave", "providers/nitro/nitro-helper"] 23 | default-members = ["providers/softsign"] 24 | -------------------------------------------------------------------------------- /Dockerfile.nitro: -------------------------------------------------------------------------------- 1 | # This dockerfile is taken from https://github.com/aws/aws-nitro-enclaves-acm/blob/main/env/enclave/Dockerfile 2 | # Modified to help build tmkms-nitro-enclave eif 3 | 4 | # Copyright 2020-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 6 | # SPDX-License-Identifier: Apache-2.0 7 | 8 | # This Docker file sets up the development environment for the eVault components 9 | # that live inside the enclave. 10 | # Note: this is not the container image used to generate the final enclave image, 11 | # but a somewhat dependency-heavy dev-time environment. The enclave image 12 | # source container will start from scratch, cherry-picking only its 13 | # required run-time dependencies from here. 14 | 15 | FROM alpine:3.12 as build_env 16 | 17 | ARG RUST_TOOLCHAIN 18 | 19 | # Force Rust dynamic linking against the Alpine-default musl libc. 20 | ENV RUSTFLAGS="-C target-feature=-crt-static" 21 | 22 | # Install system dependencies / packages. 23 | RUN apk add \ 24 | p11-kit-server \ 25 | ca-certificates \ 26 | cmake \ 27 | g++ \ 28 | gcc \ 29 | git \ 30 | go \ 31 | perl \ 32 | curl \ 33 | make \ 34 | linux-headers \ 35 | shadow \ 36 | sudo 37 | 38 | RUN ln -s /usr/lib /usr/lib64 39 | 40 | RUN mkdir -p /build 41 | WORKDIR /build 42 | 43 | # Build AWS libcrypto 44 | ENV AWS_LC_VER="v1.0.2" 45 | RUN git clone "https://github.com/awslabs/aws-lc.git" \ 46 | && cd aws-lc \ 47 | && git reset --hard $AWS_LC_VER \ 48 | && cmake \ 49 | -DCMAKE_PREFIX_PATH=/usr \ 50 | -DCMAKE_INSTALL_PREFIX=/usr \ 51 | -DBUILD_SHARED_LIBS=1 \ 52 | -DBUILD_TESTING=0 \ 53 | -B build \ 54 | && cmake --build build/ --parallel $(nproc) --target crypto \ 55 | && mv build/crypto/libcrypto.so /usr/lib/ \ 56 | && cp -rf include/openssl /usr/include/ \ 57 | && ldconfig /usr/lib 58 | 59 | # AWS-S2N 60 | ENV AWS_S2N_VER="v1.3.11" 61 | RUN git clone https://github.com/aws/s2n-tls.git \ 62 | && cd s2n-tls \ 63 | && git reset --hard $AWS_S2N_VER \ 64 | && cmake \ 65 | -DCMAKE_PREFIX_PATH=/usr \ 66 | -DCMAKE_INSTALL_PREFIX=/usr \ 67 | -DBUILD_SHARED_LIBS=1 \ 68 | -DBUILD_TESTING=0 \ 69 | -B build \ 70 | && cmake --build build/ --parallel $(nproc) --target install 71 | 72 | # AWS-C-COMMON 73 | ENV AWS_C_COMMON_VER="v0.6.20" 74 | RUN git clone https://github.com/awslabs/aws-c-common.git \ 75 | && cd aws-c-common \ 76 | && git reset --hard $AWS_C_COMMON_VER \ 77 | && cmake \ 78 | -DCMAKE_PREFIX_PATH=/usr \ 79 | -DCMAKE_INSTALL_PREFIX=/usr \ 80 | -DBUILD_SHARED_LIBS=1 \ 81 | -DBUILD_TESTING=0 \ 82 | -B build \ 83 | && cmake --build build/ --parallel $(nproc) --target install 84 | 85 | # AWS-C-SDKUTILS 86 | ENV AWS_C_SDKUTILS_VER="v0.1.2" 87 | RUN git clone https://github.com/awslabs/aws-c-sdkutils \ 88 | && cd aws-c-sdkutils \ 89 | && git reset --hard $AWS_C_SDKUTILS_VER \ 90 | && cmake \ 91 | -DCMAKE_PREFIX_PATH=/usr \ 92 | -DCMAKE_INSTALL_PREFIX=/usr \ 93 | -DBUILD_SHARED_LIBS=1 \ 94 | -DBUILD_TESTING=0 \ 95 | -B build \ 96 | && cmake --build build/ --parallel $(nproc) --target install 97 | 98 | # AWS-C-CAL 99 | ENV AWS_C_CAL_VER="v0.5.17" 100 | RUN git clone https://github.com/awslabs/aws-c-cal.git \ 101 | && cd aws-c-cal \ 102 | && git reset --hard $AWS_C_CAL_VER \ 103 | && cmake \ 104 | -DCMAKE_PREFIX_PATH=/usr \ 105 | -DCMAKE_INSTALL_PREFIX=/usr \ 106 | -DBUILD_SHARED_LIBS=1 \ 107 | -DBUILD_TESTING=0 \ 108 | -B build \ 109 | && cmake --build build --parallel $(nproc) --target install 110 | 111 | # AWS-C-IO 112 | ENV AWS_C_IO_VER="v0.10.21" 113 | RUN git clone https://github.com/awslabs/aws-c-io.git \ 114 | && cd aws-c-io \ 115 | && git reset --hard $AWS_C_IO_VER \ 116 | && cmake \ 117 | -DUSE_VSOCK=1 \ 118 | -DCMAKE_PREFIX_PATH=/usr \ 119 | -DCMAKE_INSTALL_PREFIX=/usr \ 120 | -DBUILD_SHARED_LIBS=1 \ 121 | -DBUILD_TESTING=0 \ 122 | -B build \ 123 | && cmake --build build/ --parallel $(nproc) --target install 124 | 125 | # AWS-C-COMPRESSION 126 | ENV AWS_C_COMPRESSION_VER="v0.2.14" 127 | RUN git clone http://github.com/awslabs/aws-c-compression.git \ 128 | && cd aws-c-compression \ 129 | && git reset --hard $AWS_C_COMPRESSION_VER \ 130 | && cmake \ 131 | -DCMAKE_PREFIX_PATH=/usr \ 132 | -DCMAKE_INSTALL_PREFIX=/usr \ 133 | -DBUILD_SHARED_LIBS=1 \ 134 | -DBUILD_TESTING=0 \ 135 | -B build \ 136 | && cmake --build build --parallel $(nproc) --target install 137 | 138 | # AWS-C-HTTP 139 | ENV AWS_C_HTTP_VER="v0.6.13" 140 | RUN git clone https://github.com/awslabs/aws-c-http.git \ 141 | && cd aws-c-http \ 142 | && git reset --hard $AWS_C_HTTP_VER \ 143 | && cmake \ 144 | -DCMAKE_PREFIX_PATH=/usr \ 145 | -DCMAKE_INSTALL_PREFIX=/usr \ 146 | -DBUILD_SHARED_LIBS=1 \ 147 | -DBUILD_TESTING=0 \ 148 | -B build \ 149 | && cmake --build build --parallel $(nproc) --target install 150 | 151 | # AWS-C-AUTH 152 | ENV AWS_C_AUTH_VER="v0.6.11" 153 | RUN git clone https://github.com/awslabs/aws-c-auth.git \ 154 | && cd aws-c-auth \ 155 | && git reset --hard $AWS_C_AUTH_VER \ 156 | && cmake \ 157 | -DCMAKE_PREFIX_PATH=/usr \ 158 | -DCMAKE_INSTALL_PREFIX=/usr \ 159 | -DBUILD_SHARED_LIBS=1 \ 160 | -DBUILD_TESTING=0 \ 161 | -B build \ 162 | && cmake --build build --parallel $(nproc) --target install 163 | 164 | # JSON-C library 165 | ENV JSON_C_VER="json-c-0.16-20220414" 166 | RUN git clone https://github.com/json-c/json-c.git \ 167 | && cd json-c \ 168 | && git reset --hard $JSON_C_VER \ 169 | && cmake \ 170 | -DCMAKE_PREFIX_PATH=/usr \ 171 | -DCMAKE_INSTALL_PREFIX=/usr \ 172 | -DBUILD_SHARED_LIBS=1 \ 173 | -DBUILD_TESTING=0 \ 174 | -B build \ 175 | && cmake --build build --parallel $(nproc) --target install 176 | 177 | # Install Rust 178 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUST_TOOLCHAIN 179 | 180 | # NSM LIB 181 | ENV AWS_NE_NSM_API_VER="v0.2.1" 182 | ENV CARGO_NET_GIT_FETCH_WITH_CLI=true 183 | RUN git clone "https://github.com/aws/aws-nitro-enclaves-nsm-api" \ 184 | && cd aws-nitro-enclaves-nsm-api \ 185 | && git reset --hard $AWS_NE_NSM_API_VER \ 186 | && PATH="$PATH:/root/.cargo/bin" cargo build --release -p nsm-lib \ 187 | && mv target/release/libnsm.so /usr/lib/ \ 188 | && mv target/release/nsm.h /usr/include/ 189 | 190 | # AWS Nitro Enclaves SDK 191 | ENV AWS_NE_SDK_VER="v0.2.1" 192 | RUN git clone "https://github.com/aws/aws-nitro-enclaves-sdk-c" \ 193 | && cd aws-nitro-enclaves-sdk-c \ 194 | && git reset --hard $AWS_NE_SDK_VER \ 195 | && cmake \ 196 | -DCMAKE_PREFIX_PATH=/usr \ 197 | -DCMAKE_INSTALL_PREFIX=/usr \ 198 | -DBUILD_SHARED_LIBS=1 \ 199 | -DBUILD_TESTING=0 \ 200 | -B build \ 201 | && cmake --build build --target install --parallel $(nproc) 202 | 203 | ENV PATH="/root/.cargo/bin:$PATH" 204 | 205 | FROM --platform=$TARGETPLATFORM build_env as builder 206 | 207 | ARG TARGETPLATFORM 208 | 209 | WORKDIR /tmkms-light 210 | 211 | USER root 212 | 213 | COPY . . 214 | 215 | ENV CARGO_NET_GIT_FETCH_WITH_CLI=true 216 | 217 | RUN if [ "$TARGETPLATFORM" == "linux/amd64" ]; then \ 218 | export CARGO_BUILD_TARGET=x86_64-unknown-linux-musl; \ 219 | elif [ "$TARGETPLATFORM" == "linux/arm64" ]; then \ 220 | export CARGO_BUILD_TARGET=aarch64-unknown-linux-musl; \ 221 | fi; \ 222 | cargo build \ 223 | --target=${CARGO_BUILD_TARGET} \ 224 | --package tmkms-nitro-enclave \ 225 | --release \ 226 | && cp target/${CARGO_BUILD_TARGET}/release/tmkms-nitro-enclave /usr/bin/tmkms-nitro-enclave 227 | 228 | WORKDIR /rootfs 229 | 230 | # Collect eVault lib deps 231 | RUN BINS="\ 232 | /usr/bin/tmkms-nitro-enclave \ 233 | " && \ 234 | for bin in $BINS; do \ 235 | { echo "$bin"; ldd "$bin" | grep -Eo "/.*lib.*/[^ ]+"; } | \ 236 | while read path; do \ 237 | mkdir -p ".$(dirname $path)"; \ 238 | cp -fL "$path" ".$path"; \ 239 | strip --strip-unneeded ".$path"; \ 240 | done \ 241 | done 242 | RUN mkdir -p /rootfs/etc/ssl/certs \ 243 | && cp -f /etc/ssl/certs/ca-certificates.crt /rootfs/etc/ssl/certs/ 244 | 245 | RUN mkdir -p /rootfs/bin/ 246 | 247 | RUN find /rootfs 248 | 249 | FROM alpine:3.13 250 | 251 | ARG USER=default 252 | ENV HOME /home/$USER 253 | 254 | # install sudo as root 255 | RUN apk add --update sudo 256 | 257 | # add new user 258 | RUN adduser -D $USER \ 259 | && echo "$USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USER \ 260 | && chmod 0440 /etc/sudoers.d/$USER 261 | 262 | USER $USER 263 | WORKDIR $HOME 264 | 265 | COPY --from=builder /rootfs / 266 | 267 | CMD ["/usr/bin/tmkms-nitro-enclave"] -------------------------------------------------------------------------------- /Dockerfile.sgx: -------------------------------------------------------------------------------- 1 | # TODO: use nix + libfaketime etc. 2 | # (there may still be some non-determinism in the build environment 3 | # due to `apt-get update` etc.) 4 | FROM ubuntu:jammy-20220801 as build 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | # Install system dependencies / packages. 7 | RUN apt-get update && apt-get install \ 8 | curl \ 9 | pkg-config\ 10 | libssl-dev \ 11 | clang-14 \ 12 | protobuf-compiler -y && ln -s $(which clang-14) /usr/bin/cc 13 | 14 | ARG RUST_TOOLCHAIN=nightly-2023-02-20 15 | # Install Rust (nightly is required for the `x86_64-fortanix-unknown-sgx` target) 16 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUST_TOOLCHAIN \ 17 | && /root/.cargo/bin/rustup target add x86_64-fortanix-unknown-sgx --toolchain $RUST_TOOLCHAIN 18 | ENV PATH="/root/.cargo/bin:$PATH" 19 | # SGX doesn't support the CPUID instruction, so CPU features are decided at compile-time 20 | ENV RUSTFLAGS="-Ctarget-feature=+mmx,+sse,+sse2,+sse3,+pclmulqdq,+pclmul,+ssse3,+fma,+sse4.1,+sse4.2,+popcnt,+aes,+avx,+rdrand,+sgx,+bmi1,+avx2,+bmi2,+rdseed,+adx,+sha" 21 | # actually only `fortanix-sgx-tools` is needed (`sgxs-tools` is optional for development if one wants to use e.g. `sgxs-info` in the container) 22 | RUN cargo install fortanix-sgx-tools --version 0.5.1 23 | # && cargo install sgxs-tools --version 0.8.6 24 | # right now, there shouldn't be any C dependencies in the enclave app and the Rust compiler should automatically enforce this hardening for `x86_64-fortanix-unknown-sgx` 25 | # (this is more of a reminder to watch out for this if a C dependency is added to the enclave app in the future) 26 | ENV CFLAGS="-mlvi-hardening -mllvm -x86-experimental-lvi-inline-asm-hardening" 27 | WORKDIR /tmkms-light 28 | USER root 29 | COPY . . 30 | RUN cargo build -p tmkms-light-sgx-app --release --target x86_64-fortanix-unknown-sgx \ 31 | && ftxsgx-elf2sgxs target/x86_64-fortanix-unknown-sgx/release/tmkms-light-sgx-app --heap-size 0x40000 --stack-size 0x40000 --threads 2 32 | 33 | FROM scratch AS export 34 | COPY --from=build /tmkms-light/target/x86_64-fortanix-unknown-sgx/release/tmkms-light-sgx-app.sgxs /tmkms-light-sgx-app.sgxs 35 | 36 | # extraction command: `docker build -f Dockerfile.sgx --target export --output type=local,dest=./ .` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021-present Crypto.com 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Logos/C Crypto.com (green).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/tmkms-light/4d1d394b03335be980dbe6f740e4517ccfcf42d2/Logos/C Crypto.com (green).png -------------------------------------------------------------------------------- /Logos/Lionhead Crypto.com (blue).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/tmkms-light/4d1d394b03335be980dbe6f740e4517ccfcf42d2/Logos/Lionhead Crypto.com (blue).png -------------------------------------------------------------------------------- /Logos/README.md: -------------------------------------------------------------------------------- 1 | # Logos 2 | 3 | The logos are registered trademarks of Crypto.com and any unauthorised use of the logos or its elements may constitute a breach of such trademark. The name or logos of Crypto.com may not be used or reproduced without the specific, prior written permission of Crypto.com. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Tendermint KMS Light 2 | Copyright 2021 Crypto.com. 3 | 4 | This project contains portions of code derived from the following libraries: 5 | 6 | * Tendermint KMS 7 | * Copyright: Copyright (c) 2018-2021 Iqlusion Inc. 8 | * License: Apache License 2.0 9 | * Repository: https://github.com/iqlusioninc/tmkms 10 | 11 | * AWS Certificate Manager for Nitro Enclaves 12 | * Copyright: Copyright 2020 Amazon.com, Inc. or its affiliates. 13 | * License: Apache License 2.0 14 | * Repository: https://github.com/aws/aws-nitro-enclaves-acm/ 15 | 16 | * AWS Nitro Enclaves 17 | * Copyright: Copyright 2020 Amazon.com, Inc. or its affiliates. 18 | * License: Apache License 2.0 19 | * Repository: https://github.com/aws/aws-nitro-enclaves-cli 20 | 21 | * tokio-rs/tracing 22 | * Copyright: Copyright (c) 2019 Tokio Contributors 23 | * License: MIT License 24 | * Repository: https://github.com/tokio-rs/tracing 25 | 26 | * AWS Nitro Enclaves Python demo 27 | * Copyright: Copyright 2020 Richard Fan 28 | * License: Apache License 2.0 29 | * Repository: https://github.com/richardfan1126/nitro-enclave-python-demo 30 | 31 | * bech32 32 | * Copyright: Copyright (c) 2017 Pieter Wuille 33 | * License: MIT License 34 | * Repository: https://github.com/fiatjaf/bech32 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Coordinated Vulnerability Disclosure Policy 2 | We ask security researchers to keep vulnerabilities and communications around vulnerability submissions private and confidential until a patch is developed to protect the people using Crypto.com’s protocols. In addition to this, we ask that you: 3 | 4 | - Allow us a reasonable amount of time to correct or address security vulnerabilities. 5 | - Avoid exploiting any vulnerabilities that you discover. 6 | - Demonstrate good faith by not disrupting or degrading Crypto.com’s data or services. 7 | 8 | ## Vulnerability Disclosure Process 9 | Once we receive a vulnerability report, Crypto.com will take these steps to address it: 10 | 11 | 1. Crypto.com will confirm receipt of the vulnerability report within 5 business days. The timing of our response may depend on when a report is submitted. As our daily operations are distributed in time zones across the globe, response times may vary. If you have not received a response to a vulnerability report from us within 5 business days, we encourage you to follow up with us again for a response. 12 | 2. Crypto.com will investigate and validate the security issue submitted to us as quickly as we can, usually within 10 business days of receipt. Submitting a thorough report with clear steps to recreate the vulnerability and/or a proof-of-concept will move the process along in a timely manner. 13 | 3. Crypto.com will acknowledge the bug, and make the necessary code changes to patch it. Some issues may require more time than others to patch, but we will strive to patch each vulnerability as quickly as our resources and development process allow. 14 | 4. Crypto.com will publicly release the security patch for the vulnerability, and acknowledge the security fix in the release notes once the issue has been resolved. Public release notes can reference to the person or people who reported the vulnerability, unless they wish to stay anonymous. 15 | 16 | ## Contact Us 17 | If you find a security issue, you can report it on the [Crypto.com HackerOne Bug Bounty Program](https://hackerone.com/crypto) or you can contact our team directly at [chain-security@crypto.com](mailto:chain-security@crypto.com). 18 | To communicate sensitive information, you can use the latest key in the 19 | [cryptocom's Keybase account](https://keybase.io/cryptocom/pgp_keys.asc) or use its [chat functionality](https://keybase.io/cryptocom/chat). -------------------------------------------------------------------------------- /integration-test/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ./nix { } 2 | }: 3 | pkgs.poetry2nix.mkPoetryApplication { 4 | projectDir = ./.; 5 | } -------------------------------------------------------------------------------- /integration-test/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1634851050, 6 | "narHash": "sha256-N83GlSGPJJdcqhUxSCS/WwW5pksYf3VP1M13cDRTSVA=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "c91f3de5adaf1de973b797ef7485e441a65b8935", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1636450945, 21 | "narHash": "sha256-hCMerNfpkYOBHetWtS1oy6yc8rdMpdBOHRwqKZ+gRiM=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "cd465ef283ae81945426c5b885af615edc346880", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "NixOS", 29 | "repo": "nixpkgs", 30 | "type": "github" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | } 39 | }, 40 | "root": "root", 41 | "version": 7 42 | } 43 | -------------------------------------------------------------------------------- /integration-test/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "integration tests"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | 14 | customOverrides = self: super: { 15 | # Overrides go here 16 | }; 17 | 18 | app = pkgs.poetry2nix.mkPoetryApplication { 19 | projectDir = ./.; 20 | overrides = 21 | [ pkgs.poetry2nix.defaultPoetryOverrides customOverrides ]; 22 | }; 23 | 24 | packageName = "integration-tests"; 25 | in { 26 | packages.${packageName} = app; 27 | 28 | defaultPackage = self.packages.${system}.${packageName}; 29 | 30 | devShell = pkgs.mkShell { 31 | buildInputs = with pkgs; [ poetry bash ]; 32 | inputsFrom = builtins.attrValues self.packages.${system}; 33 | }; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /integration-test/integration_test/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /integration-test/integration_test/bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import fire 3 | import tomlkit 4 | import os 5 | from pathlib import Path 6 | import subprocess 7 | import json 8 | 9 | class CLI: 10 | def prepare(self, tendermint="tendermint", tmkms="tmkms-softsign", tmhome=".tendermint", kmsconfig="tmkms.toml"): 11 | '''Prepare tendermint node with tmkms executable 12 | :param tendermint: Path of tendermint, [default: tendermint] 13 | :param tmkms: Path of tmkms-light, [default: tmkms-softsign] 14 | :param tmhome: Path of tendermint home path, [default: .tendermint] 15 | :param kmsconfig: Path of tmkms.toml, [default: tmkms.toml] 16 | 17 | ''' 18 | tendermint = tendermint if tendermint else "tendermint" 19 | tmkms = tmkms if tmkms else "tmkms-softsign" 20 | tmhome = tmhome if tmhome else ".tendermint" 21 | kmsconfig = kmsconfig if kmsconfig else "tmkms.toml" 22 | chainid = "testchain-1" 23 | privsock = "unix:///tmp/test.socket" 24 | tm_config = tmhome + "/config/config.toml" 25 | genesis_path = tmhome + "/config/genesis.json" 26 | 27 | os.system(tendermint + " init --home " + tmhome) 28 | node0 = tomlkit.parse(Path(tm_config).read_text()) 29 | node0["proxy_app"] = "counter" 30 | node0["priv_validator_laddr"] = privsock 31 | open(tm_config, "w").write(tomlkit.dumps(node0)) 32 | os.system(tmkms + " init -c " + kmsconfig) 33 | node0 = tomlkit.parse(Path(kmsconfig).read_text()) 34 | node0["address"] = privsock 35 | node0["chain_id"] = chainid 36 | open(kmsconfig, "w").write(tomlkit.dumps(node0)) 37 | process = subprocess.Popen([tmkms, "pubkey", "-c", kmsconfig], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 38 | stdout, _stderr = process.communicate() 39 | parts = stdout.split() 40 | encoding = "utf-8" 41 | address = parts[4].decode(encoding) 42 | pubkey = parts[2].decode(encoding) 43 | genesis = json.loads(Path(genesis_path).read_text()) 44 | genesis["validators"][0]["address"] = address 45 | genesis["validators"][0]["pub_key"]["value"] = pubkey 46 | genesis["chain_id"] = chainid 47 | open(genesis_path, "w").write(json.dumps(genesis)) 48 | 49 | 50 | 51 | 52 | 53 | if __name__ == '__main__': 54 | fire.Fire(CLI()) 55 | -------------------------------------------------------------------------------- /integration-test/nix/default.nix: -------------------------------------------------------------------------------- 1 | { sources ? import ./sources.nix }: 2 | import sources.nixpkgs { 3 | overlays = [ ]; 4 | config = { }; 5 | } -------------------------------------------------------------------------------- /integration-test/nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "nixpkgs": { 3 | "branch": "nixos-20.09", 4 | "description": "Nix Packages collection", 5 | "homepage": "", 6 | "owner": "NixOS", 7 | "repo": "nixpkgs", 8 | "rev": "60b18a066e8ce5dd21ebff5324345d3586a67ad9", 9 | "sha256": "1cr7r16vb4nxifykhak8awspdpr775kafrl7p6asgwicsng7giya", 10 | "type": "tarball", 11 | "url": "https://github.com/NixOS/nixpkgs/archive/60b18a066e8ce5dd21ebff5324345d3586a67ad9.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration-test/nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: spec: 10 | if spec.builtin or true then 11 | builtins_fetchurl { inherit (spec) url sha256; } 12 | else 13 | pkgs.fetchurl { inherit (spec) url sha256; }; 14 | 15 | fetch_tarball = pkgs: name: spec: 16 | let 17 | ok = str: ! builtins.isNull (builtins.match "[a-zA-Z0-9+-._?=]" str); 18 | # sanitize the name, though nix will still fail if name starts with period 19 | name' = stringAsChars (x: if ! ok x then "-" else x) "${name}-src"; 20 | in 21 | if spec.builtin or true then 22 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 23 | else 24 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 25 | 26 | fetch_git = spec: 27 | builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; }; 28 | 29 | fetch_local = spec: spec.path; 30 | 31 | fetch_builtin-tarball = name: throw 32 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 33 | $ niv modify ${name} -a type=tarball -a builtin=true''; 34 | 35 | fetch_builtin-url = name: throw 36 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 37 | $ niv modify ${name} -a type=file -a builtin=true''; 38 | 39 | # 40 | # Various helpers 41 | # 42 | 43 | # The set of packages used when specs are fetched using non-builtins. 44 | mkPkgs = sources: 45 | let 46 | sourcesNixpkgs = 47 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {}; 48 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 49 | hasThisAsNixpkgsPath = == ./.; 50 | in 51 | if builtins.hasAttr "nixpkgs" sources 52 | then sourcesNixpkgs 53 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 54 | import {} 55 | else 56 | abort 57 | '' 58 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 59 | add a package called "nixpkgs" to your sources.json. 60 | ''; 61 | 62 | # The actual fetching function. 63 | fetch = pkgs: name: spec: 64 | 65 | if ! builtins.hasAttr "type" spec then 66 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 67 | else if spec.type == "file" then fetch_file pkgs spec 68 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 69 | else if spec.type == "git" then fetch_git spec 70 | else if spec.type == "local" then fetch_local spec 71 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 72 | else if spec.type == "builtin-url" then fetch_builtin-url name 73 | else 74 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 75 | 76 | # If the environment variable NIV_OVERRIDE_${name} is set, then use 77 | # the path directly as opposed to the fetched source. 78 | replace = name: drv: 79 | let 80 | saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; 81 | ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; 82 | in 83 | if ersatz == "" then drv else ersatz; 84 | 85 | # Ports of functions for older nix versions 86 | 87 | # a Nix version of mapAttrs if the built-in doesn't exist 88 | mapAttrs = builtins.mapAttrs or ( 89 | f: set: with builtins; 90 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 91 | ); 92 | 93 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 94 | range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); 95 | 96 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 97 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 98 | 99 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 100 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 101 | concatStrings = builtins.concatStringsSep ""; 102 | 103 | # fetchTarball version that is compatible between all the versions of Nix 104 | builtins_fetchTarball = { url, name, sha256 }@attrs: 105 | let 106 | inherit (builtins) lessThan nixVersion fetchTarball; 107 | in 108 | if lessThan nixVersion "1.12" then 109 | fetchTarball { inherit name url; } 110 | else 111 | fetchTarball attrs; 112 | 113 | # fetchurl version that is compatible between all the versions of Nix 114 | builtins_fetchurl = { url, sha256 }@attrs: 115 | let 116 | inherit (builtins) lessThan nixVersion fetchurl; 117 | in 118 | if lessThan nixVersion "1.12" then 119 | fetchurl { inherit url; } 120 | else 121 | fetchurl attrs; 122 | 123 | # Create the final "sources" from the config 124 | mkSources = config: 125 | mapAttrs ( 126 | name: spec: 127 | if builtins.hasAttr "outPath" spec 128 | then abort 129 | "The values in sources.json should not have an 'outPath' attribute" 130 | else 131 | spec // { outPath = replace name (fetch config.pkgs name spec); } 132 | ) config.sources; 133 | 134 | # The "config" used by the fetchers 135 | mkConfig = 136 | { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null 137 | , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) 138 | , pkgs ? mkPkgs sources 139 | }: rec { 140 | # The sources, i.e. the attribute set of spec name to spec 141 | inherit sources; 142 | 143 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 144 | inherit pkgs; 145 | }; 146 | 147 | in 148 | mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } 149 | -------------------------------------------------------------------------------- /integration-test/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "integration-test" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Tomas Tauber <2410580+tomtau@users.noreply.github.com>"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | fire = {git = "https://github.com/google/python-fire.git"} 10 | tomlkit = "^0.7.0" 11 | jsonmerge = "^1.7.0" 12 | supervisor = "^4.2.1" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "^5.2" 16 | 17 | [build-system] 18 | requires = ["poetry>=0.12"] 19 | build-backend = "poetry.masonry.api" 20 | -------------------------------------------------------------------------------- /integration-test/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | ./tmkms-softsign init 4 | wget https://github.com/tendermint/tendermint/releases/download/v0.34.8/tendermint_0.34.8_linux_amd64.tar.gz 5 | tar xvfz tendermint_0.34.8_linux_amd64.tar.gz 6 | chmod +x tendermint 7 | export TENDERMINT=./tendermint 8 | export TMHOME=.tendermint 9 | export TMKMS=./tmkms-softsign 10 | export TMKMSCONFIG=tmkms.toml 11 | python integration_test/bot.py prepare --tendermint ./tendermint --tmhome .tendermint --tmkms ./tmkms-softsign --kmsconfig tmkms.toml 12 | pytest -v -------------------------------------------------------------------------------- /integration-test/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crypto-com/tmkms-light/4d1d394b03335be980dbe6f740e4517ccfcf42d2/integration-test/tests/__init__.py -------------------------------------------------------------------------------- /integration-test/tests/test_integration_test.py: -------------------------------------------------------------------------------- 1 | from integration_test import __version__ 2 | import os 3 | import subprocess 4 | import urllib.request 5 | import json 6 | import time 7 | from pathlib import Path 8 | 9 | def test_basic(): 10 | tm = os.getenv('TENDERMINT') 11 | tmhome = os.getenv('TMHOME') 12 | tmkms = os.getenv('TMKMS') 13 | kmsconfig = os.getenv('TMKMSCONFIG') 14 | tmkms_proc = subprocess.Popen([tmkms, "start", "-c", kmsconfig], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 15 | tm_proc = subprocess.Popen([tm, "node", "--home", tmhome], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 16 | contents = None 17 | start_time = time.perf_counter() 18 | timeout = 30 19 | rpc_base = "http://127.0.0.1:26657" 20 | status_url = rpc_base + "/status" 21 | block_url = rpc_base + "/block" 22 | while True: 23 | try: 24 | contents = urllib.request.urlopen(status_url).read() 25 | break 26 | except Exception as e: 27 | time.sleep(1) 28 | if time.perf_counter() - start_time >= timeout: 29 | print(e) 30 | tm_output = tm_proc.stdout.readlines() 31 | os.system("pkill -9 " + tmkms) 32 | tmkms_output = tmkms_proc.stdout.readlines() 33 | tmkms_err = tmkms_proc.stderr.readlines() 34 | raise TimeoutError('Waited too long for the RPC port.\n tm: {}\ntmkms output:{}\ntmkms error: {}'.format(tm_output, tmkms_output, tmkms_err)) from e 35 | time.sleep(5) 36 | contents = urllib.request.urlopen(status_url).read() 37 | status = json.loads(contents) 38 | block_height = int(status["result"]["sync_info"]["latest_block_height"]) 39 | assert block_height >= 1 40 | contents = urllib.request.urlopen(block_url).read() 41 | block = json.loads(contents) 42 | validator_address = block['result']['block']['last_commit']['signatures'][0]['validator_address'] 43 | genesis_path = tmhome + "/config/genesis.json" 44 | genesis = json.loads(Path(genesis_path).read_text()) 45 | assert validator_address == genesis["validators"][0]["address"].upper() -------------------------------------------------------------------------------- /providers/nitro/nitro-enclave/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmkms-nitro-enclave" 3 | version = "0.4.2" 4 | authors = ["Tomas Tauber <2410580+tomtau@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | aws-ne-sys = "0.4" 9 | aws-nitro-enclaves-nsm-api = "0.2" 10 | ed25519-consensus = "2" 11 | flex-error = "0.4" 12 | nix = "0.26" 13 | rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } 14 | serde_bytes = "0.11" 15 | serde_json = "1" 16 | subtle = "2" 17 | subtle-encoding = "0.5" 18 | tendermint = "0.30" 19 | tendermint-p2p = "0.30" 20 | tmkms-light = { path = "../../.." } 21 | tmkms-nitro-helper = { path = "../nitro-helper", default-features = false } 22 | tracing = "0.1" 23 | tracing-subscriber = "0.3" 24 | vsock = "0.3" 25 | zeroize = "1" 26 | -------------------------------------------------------------------------------- /providers/nitro/nitro-enclave/src/main.rs: -------------------------------------------------------------------------------- 1 | use tracing::Level; 2 | use tracing::{error, info, warn}; 3 | use tracing_subscriber::fmt; 4 | use tracing_subscriber::layer::SubscriberExt; 5 | use vsock::{VsockAddr, VsockListener}; 6 | 7 | use tmkms_nitro_helper::tracing_layer::Layer; 8 | use tmkms_nitro_helper::VSOCK_HOST_CID; 9 | use tracing_subscriber::filter::LevelFilter; 10 | 11 | mod nitro; 12 | 13 | fn main() { 14 | let mut env_args = std::env::args(); 15 | let port = env_args 16 | .next() 17 | .and_then(|x| x.parse::().ok()) 18 | .unwrap_or(5050); 19 | 20 | let log_server_port = env_args 21 | .next() 22 | .and_then(|x| x.parse::().ok()) 23 | .unwrap_or(6050); 24 | 25 | let log_level = env_args 26 | .next() 27 | .map(|x| { 28 | if x.to_lowercase() == "--verbose" || x.to_lowercase() == "-v" { 29 | Level::INFO 30 | } else { 31 | Level::DEBUG 32 | } 33 | }) 34 | .unwrap_or_else(|| Level::INFO); 35 | let log_layer = LevelFilter::from(log_level); 36 | let layer = Layer::new(VSOCK_HOST_CID, log_server_port); 37 | let fmt_layer = fmt::layer().with_target(false); 38 | let layered = tracing_subscriber::registry() 39 | .with(log_layer) 40 | .with(fmt_layer) 41 | .with(layer); 42 | 43 | tracing::subscriber::set_global_default(layered).expect("setting default subscriber failed"); 44 | 45 | const VMADDR_CID_ANY: u32 = 0xFFFFFFFF; 46 | let addr = VsockAddr::new(VMADDR_CID_ANY, port); 47 | let listener = VsockListener::bind(&addr).expect("bind address"); 48 | info!("waiting for config to be pushed on {}", addr); 49 | for conn in listener.incoming() { 50 | if aws_ne_sys::seed_entropy(512).is_err() { 51 | error!("failed to seed initial entropy!"); 52 | std::process::exit(1); 53 | } 54 | match conn { 55 | Ok(stream) => { 56 | info!("got connection on {:?}", addr); 57 | if let Err(e) = nitro::entry(stream) { 58 | error!("io error {}", e); 59 | } 60 | } 61 | Err(e) => { 62 | warn!("connection error {}", e); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /providers/nitro/nitro-enclave/src/nitro/state.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::os::unix::io::AsRawFd; 3 | use tmkms_light::chain::state::{consensus, PersistStateSync, State, StateError}; 4 | use tmkms_light::utils::{read_u16_payload, write_u16_payload}; 5 | use tmkms_nitro_helper::VSOCK_HOST_CID; 6 | use tracing::{debug, trace}; 7 | use vsock::{VsockAddr, VsockStream}; 8 | 9 | /// as the state needs to be persisted outside of NE, 10 | /// this is a helper that communicates with the host to load the latest state 11 | /// on the start up + to update it after each signing 12 | #[derive(Debug, Clone)] 13 | pub struct StateHolder { 14 | state_conn: VsockStream, 15 | } 16 | 17 | impl StateHolder { 18 | /// connects to the host via the vsock port specified in the configuration 19 | pub fn new(vsock_port: u32) -> io::Result { 20 | let addr = VsockAddr::new(VSOCK_HOST_CID, vsock_port); 21 | let state_conn = vsock::VsockStream::connect(&addr)?; 22 | trace!("state vsock port: {}", vsock_port); 23 | trace!("state peer addr: {:?}", state_conn.peer_addr()); 24 | trace!("state local addr: {:?}", state_conn.local_addr()); 25 | trace!("state fd: {}", state_conn.as_raw_fd()); 26 | Ok(Self { state_conn }) 27 | } 28 | } 29 | 30 | impl PersistStateSync for StateHolder { 31 | /// loads the initial state 32 | fn load_state(&mut self) -> Result { 33 | let json_raw = read_u16_payload(&mut self.state_conn) 34 | .map_err(|e| StateError::sync_other_error(e.to_string()))?; 35 | let consensus_state: consensus::State = serde_json::from_slice(&json_raw) 36 | .map_err(|e| StateError::sync_enc_dec_error("vsock".into(), e))?; 37 | Ok(State::from(consensus_state)) 38 | } 39 | 40 | /// sends the update state to be persisted on the host 41 | fn persist_state(&mut self, new_state: &consensus::State) -> Result<(), StateError> { 42 | trace!("writing new consensus state to state conn"); 43 | trace!("state peer addr: {:?}", self.state_conn.peer_addr()); 44 | trace!("state local addr: {:?}", self.state_conn.local_addr()); 45 | trace!("state fd: {}", self.state_conn.as_raw_fd()); 46 | let json_raw = serde_json::to_vec(&new_state) 47 | .map_err(|e| StateError::sync_enc_dec_error("vsock".into(), e))?; 48 | 49 | write_u16_payload(&mut self.state_conn, &json_raw) 50 | .map_err(|e| StateError::sync_error("vsock".into(), e))?; 51 | 52 | debug!("successfully wrote new consensus state to state connection"); 53 | 54 | Ok(()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmkms-nitro-helper" 3 | version = "0.4.2" 4 | authors = [ "Tomas Tauber <2410580+tomtau@users.noreply.github.com>" ] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | aws-config = "0.54" 9 | aws-credential-types = "0.54" 10 | ctrlc = "3" 11 | ed25519-consensus = "2" 12 | flex-error = "0.4" 13 | nix = "0.26" 14 | serde = { version = "1", features = [ "derive" ] } 15 | serde_json = "1" 16 | clap = {version = "4", features = ["derive"] } 17 | subtle-encoding = { version = "0.5", features = [ "bech32-preview" ] } 18 | sysinfo = "0.28" 19 | tempfile = "3" 20 | tendermint = "0.30" 21 | tendermint-config = "0.30" 22 | tmkms-light = { path = "../../.." } 23 | tokio = { version = "1", features = [ "rt" ] } 24 | toml = "0.7" 25 | tracing = "0.1" 26 | tracing-subscriber = "0.3" 27 | tracing-core = "0.1" 28 | vsock = "0.3" -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/command.rs: -------------------------------------------------------------------------------- 1 | pub mod launch_all; 2 | pub mod nitro_enclave; 3 | 4 | use std::sync::mpsc::Receiver; 5 | use std::{fs, path::PathBuf}; 6 | use sysinfo::{ProcessExt, SystemExt}; 7 | use tendermint_config::net; 8 | use tmkms_light::utils::write_u16_payload; 9 | use tmkms_light::utils::{print_pubkey, PubkeyDisplay}; 10 | use vsock::VsockAddr; 11 | 12 | use crate::command::nitro_enclave::describe_enclave; 13 | use crate::config::{EnclaveConfig, EnclaveOpt, NitroSignOpt, VSockProxyOpt}; 14 | use crate::key_utils::{credential, generate_key}; 15 | use crate::proxy::Proxy; 16 | use crate::shared::{NitroConfig, NitroRequest}; 17 | use crate::state::StateSyncer; 18 | 19 | /// write tmkms.toml + enclave.toml + generate keys 20 | /// config_dir: the directory that put the generated config file 21 | pub fn init( 22 | config_dir: PathBuf, 23 | pubkey_display: Option, 24 | bech32_prefix: Option, 25 | aws_region: String, 26 | kms_key_id: String, 27 | cid: Option, 28 | ) -> Result<(), String> { 29 | if !config_dir.is_dir() || !config_dir.exists() { 30 | return Err("config path is not a directory or not exists".to_string()); 31 | } 32 | let cp_helper = config_dir.join("tmkms.toml"); 33 | let cp_enclave = config_dir.join("enclave.toml"); 34 | 35 | let nitro_sign_opt = NitroSignOpt { 36 | aws_region: aws_region.clone(), 37 | ..Default::default() 38 | }; 39 | let enclave_opt = EnclaveOpt::default(); 40 | let proxy_opt = VSockProxyOpt { 41 | remote_addr: format!("kms.{}.amazonaws.com", aws_region), 42 | ..Default::default() 43 | }; 44 | let enclave_config = EnclaveConfig { 45 | enclave: enclave_opt, 46 | vsock_proxy: proxy_opt, 47 | }; 48 | let t = toml::to_string_pretty(&nitro_sign_opt) 49 | .map_err(|e| format!("failed to create a config in toml: {:?}", e))?; 50 | let t_enclave_config = toml::to_string(&enclave_config) 51 | .map_err(|e| format!("failed to create a config in toml: {:?}", e))?; 52 | fs::write(cp_helper, t).map_err(|e| format!("failed to write a config: {:?}", e))?; 53 | fs::write(cp_enclave, t_enclave_config) 54 | .map_err(|e| format!("failed to write a launch all config: {:?}", e))?; 55 | let config = nitro_sign_opt; 56 | let (cid, port) = if let Some(cid) = cid { 57 | (cid, config.enclave_config_port) 58 | } else { 59 | (config.enclave_config_cid, config.enclave_config_port) 60 | }; 61 | let credentials = if let Some(credentials) = config.credentials { 62 | credentials 63 | } else { 64 | credential::get_credentials()? 65 | }; 66 | fs::create_dir_all( 67 | config 68 | .sealed_consensus_key_path 69 | .parent() 70 | .ok_or_else(|| "cannot create a dir in a root directory".to_owned())?, 71 | ) 72 | .map_err(|e| format!("failed to create dirs for key storage: {:?}", e))?; 73 | fs::create_dir_all( 74 | config 75 | .state_file_path 76 | .parent() 77 | .ok_or_else(|| "cannot create a dir in a root directory".to_owned())?, 78 | ) 79 | .map_err(|e| format!("failed to create dirs for state storage: {:?}", e))?; 80 | 81 | // check if enclave and vsock proxy is running 82 | if !describe_enclave()? 83 | .into_iter() 84 | .any(|x| x.enclave_cid == cid as u64) 85 | { 86 | return Err("can't find running enclave with matched cid. Please use tmkms-nitro-helper run command".to_owned()); 87 | } 88 | if !check_vsock_proxy() { 89 | return Err("vsock proxy is not running, Please run vsock-proxy 8000 kms.{{ kms_region }}.amazonaws.com 443 &".to_owned()); 90 | } 91 | 92 | let (pubkey, attestation_doc) = generate_key( 93 | cid, 94 | port, 95 | config.sealed_consensus_key_path, 96 | &config.aws_region, 97 | credentials.clone(), 98 | kms_key_id.clone(), 99 | ) 100 | .map_err(|e| format!("failed to generate a key: {:?}", e))?; 101 | print_pubkey(bech32_prefix, pubkey_display, pubkey); 102 | let encoded_attdoc = String::from_utf8(subtle_encoding::base64::encode(attestation_doc)) 103 | .map_err(|e| format!("enconding attestation doc: {:?}", e))?; 104 | println!("Nitro Enclave attestation:\n{}", &encoded_attdoc); 105 | 106 | if let Some(id_path) = config.sealed_id_key_path { 107 | generate_key( 108 | cid, 109 | port, 110 | id_path, 111 | &config.aws_region, 112 | credentials, 113 | kms_key_id, 114 | ) 115 | .map_err(|e| format!("failed to generate a sealed id key: {:?}", e))?; 116 | } 117 | Ok(()) 118 | } 119 | 120 | pub fn check_vsock_proxy() -> bool { 121 | let mut system = sysinfo::System::new_all(); 122 | system.refresh_all(); 123 | system.processes().iter().any(|(_pid, p)| { 124 | let cmd = p.cmd(); 125 | cmd.iter().any(|x| x.contains("vsock-proxy")) 126 | }) 127 | } 128 | 129 | /// push config to enclave, start up a proxy (if needed) + state syncer 130 | /// stop_sync_rx: when get data from it, the sync thread will be finished 131 | pub fn start( 132 | config: &NitroSignOpt, 133 | cid: Option, 134 | stop_sync_rx: Receiver<()>, 135 | ) -> Result<(), String> { 136 | tracing::debug!("start helper with config: {:?}, cid: {:?}", config, cid); 137 | let credentials = if let Some(credentials) = &config.credentials { 138 | credentials.clone() 139 | } else { 140 | credential::get_credentials()? 141 | }; 142 | let peer_id = match config.address { 143 | net::Address::Tcp { peer_id, .. } => peer_id, 144 | _ => None, 145 | }; 146 | let state_syncer = StateSyncer::new(config.state_file_path.clone(), config.enclave_state_port) 147 | .map_err(|e| format!("failed to get a state syncing helper: {:?}", e))?; 148 | let sealed_consensus_key = fs::read(config.sealed_consensus_key_path.clone()) 149 | .map_err(|e| format!("failed to read a sealed consensus key: {:?}", e))?; 150 | let sealed_id_key = if let Some(p) = &config.sealed_id_key_path { 151 | if let net::Address::Tcp { .. } = config.address { 152 | Some( 153 | fs::read(p) 154 | .map_err(|e| format!("failed to read a sealed identity key: {:?}", e))?, 155 | ) 156 | } else { 157 | None 158 | } 159 | } else { 160 | None 161 | }; 162 | let enclave_config = NitroConfig { 163 | chain_id: config.chain_id.clone(), 164 | max_height: config.max_height, 165 | sealed_consensus_key, 166 | sealed_id_key, 167 | peer_id, 168 | enclave_state_port: config.enclave_state_port, 169 | enclave_tendermint_conn: config.enclave_tendermint_conn, 170 | credentials, 171 | aws_region: config.aws_region.clone(), 172 | }; 173 | let addr = if let Some(cid) = cid { 174 | VsockAddr::new(cid, config.enclave_config_port) 175 | } else { 176 | VsockAddr::new(config.enclave_config_cid, config.enclave_config_port) 177 | }; 178 | let mut socket = vsock::VsockStream::connect(&addr).map_err(|e| { 179 | format!( 180 | "failed to connect to the enclave to push its config: {:?}", 181 | e 182 | ) 183 | })?; 184 | let request = NitroRequest::Start(enclave_config); 185 | let config_raw = serde_json::to_vec(&request) 186 | .map_err(|e| format!("failed to serialize the config: {:?}", e))?; 187 | write_u16_payload(&mut socket, &config_raw) 188 | .map_err(|e| format!("failed to write the config: {:?}", e))?; 189 | let proxy = match &config.address { 190 | net::Address::Unix { path } => { 191 | tracing::debug!( 192 | "{}: Creating a proxy {}...", 193 | &config.chain_id, 194 | &config.address 195 | ); 196 | 197 | Some(Proxy::new( 198 | config.enclave_tendermint_conn, 199 | PathBuf::from(path), 200 | )) 201 | } 202 | _ => None, 203 | }; 204 | if let Some(p) = proxy { 205 | p.launch_proxy(); 206 | } 207 | 208 | // state syncing runs in an infinite loop (so does the proxy) 209 | state_syncer 210 | .launch_syncer(stop_sync_rx) 211 | .join() 212 | .map_err(|_| "join thread error".to_string())?; 213 | Ok(()) 214 | } 215 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/command/launch_all.rs: -------------------------------------------------------------------------------- 1 | use crate::command::nitro_enclave::run_vsock_proxy; 2 | use crate::command::nitro_enclave::{describe_enclave, run_enclave}; 3 | use crate::command::start; 4 | use crate::config::{EnclaveConfig, NitroSignOpt}; 5 | use std::sync::mpsc::{channel, Sender}; 6 | use std::thread::{self, sleep}; 7 | use std::time::Duration; 8 | 9 | pub struct Launcher { 10 | tmkms_config: NitroSignOpt, 11 | enclave_config: EnclaveConfig, 12 | stop_senders: Vec>, 13 | } 14 | 15 | impl Launcher { 16 | /// create a new launcher, stop_enclave_sender: before the launcher exit, send the signal to 17 | /// the subprocess so that it can stop gracefully. 18 | pub fn new(tmkms_config: NitroSignOpt, enclave_config: EnclaveConfig) -> Self { 19 | Self { 20 | tmkms_config, 21 | enclave_config, 22 | stop_senders: vec![], 23 | } 24 | } 25 | 26 | /// 1. run enclave 27 | /// 2. launch proxy 28 | /// 3. start helper 29 | pub fn run(&mut self) -> Result<(), String> { 30 | // create stop signal (tx,rx) 31 | let (tx1, rx1) = channel(); 32 | let (tx2, rx2) = channel(); 33 | let (tx3, rx3) = channel(); 34 | self.stop_senders.push(tx1); 35 | self.stop_senders.push(tx2); 36 | self.stop_senders.push(tx3); 37 | 38 | let mut threads = vec![]; 39 | 40 | // start enclave 41 | let enclave_config = self.enclave_config.enclave.clone(); 42 | let stop_senders = self.stop_senders.clone(); 43 | let t1 = thread::spawn(move || { 44 | tracing::info!("starting enclave ..."); 45 | if let Err(e) = run_enclave(&enclave_config, rx1) { 46 | tracing::error!("enclave error: {:?}", e); 47 | for tx in stop_senders { 48 | if let Err(e) = tx.send(()) { 49 | tracing::error!("send stop signal error: {:?}", e); 50 | std::process::exit(1); 51 | } 52 | } 53 | } 54 | }); 55 | threads.push(t1); 56 | 57 | // launch proxy 58 | let proxy_config = self.enclave_config.vsock_proxy.clone(); 59 | let stop_senders = self.stop_senders.clone(); 60 | let t2 = thread::spawn(move || { 61 | tracing::info!("starting vsock proxy"); 62 | if let Err(e) = run_vsock_proxy(&proxy_config, rx2) { 63 | tracing::error!("vsock proxy error: {:?}", e); 64 | for tx in stop_senders { 65 | if let Err(e) = tx.send(()) { 66 | tracing::error!("send stop signal error: {:?}", e); 67 | std::process::exit(1); 68 | } 69 | } 70 | } 71 | }); 72 | threads.push(t2); 73 | 74 | // run helper 75 | // check if enclave is running 76 | tracing::info!("starting helper, waiting for the enclave running..."); 77 | let timeout = 15; 78 | let mut t = 0; 79 | let cid = loop { 80 | let enclave_info = describe_enclave()?; 81 | if enclave_info.is_empty() { 82 | tracing::warn!("can't find running enclave"); 83 | } else { 84 | tracing::info!("find running enclave"); 85 | break Some(enclave_info[0].enclave_cid as u32); 86 | } 87 | t += 1; 88 | if t >= timeout { 89 | tracing::error!("can't find running enclave or start enclave timeout"); 90 | for tx in self.stop_senders.iter() { 91 | if let Err(e) = tx.send(()) { 92 | tracing::error!("send stop signal error: {:?}", e); 93 | std::process::exit(1); 94 | } 95 | } 96 | break None; 97 | } 98 | sleep(Duration::from_secs(1)); 99 | }; 100 | 101 | if cid.is_some() { 102 | let tmkms_config = self.tmkms_config.clone(); 103 | let stop_senders = self.stop_senders.clone(); 104 | let t3 = thread::spawn(move || { 105 | if let Err(e) = start(&tmkms_config, cid, rx3) { 106 | tracing::error!("{}", e); 107 | for tx in stop_senders { 108 | if let Err(e) = tx.send(()) { 109 | tracing::error!("send stop signal error: {:?}", e); 110 | std::process::exit(1); 111 | } 112 | } 113 | } 114 | }); 115 | threads.push(t3); 116 | } 117 | 118 | // when get the ctrlc signal, send stop signal 119 | let stop_senders = self.stop_senders.clone(); 120 | ctrlc::set_handler(move || { 121 | tracing::debug!("get Ctrl-C signal, send close enclave signal"); 122 | for tx in stop_senders.iter() { 123 | if let Err(e) = tx.send(()) { 124 | tracing::error!("send stop signal error: {:?}", e); 125 | } 126 | } 127 | }) 128 | .map_err(|_| "Error to set Ctrl-C channel".to_string())?; 129 | 130 | for t in threads.into_iter() { 131 | if let Err(e) = t.join() { 132 | tracing::error!("join thread error: {:?}", e); 133 | } 134 | } 135 | Ok(()) 136 | } 137 | } 138 | 139 | pub fn launch_all(tmkms_config: NitroSignOpt, enclave_config: EnclaveConfig) -> Result<(), String> { 140 | let mut launcher = Launcher::new(tmkms_config, enclave_config); 141 | launcher.run()?; 142 | Ok(()) 143 | } 144 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/command/nitro_enclave.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 3 | 4 | use crate::command::check_vsock_proxy; 5 | use crate::config::{EnclaveOpt, VSockProxyOpt}; 6 | use crate::enclave_log_server::LogServer; 7 | use serde::de::DeserializeOwned; 8 | use serde::{Deserialize, Serialize}; 9 | use std::process::{Command, Output}; 10 | use std::sync::mpsc::Receiver; 11 | 12 | /// The information provided by a `describe-enclaves` request. 13 | #[derive(Clone, Serialize, Deserialize)] 14 | pub struct EnclaveDescribeInfo { 15 | #[serde(rename = "EnclaveID")] 16 | /// The full ID of the enclave. 17 | pub enclave_id: String, 18 | #[serde(rename = "ProcessID")] 19 | /// The PID of the enclave process which manages the enclave. 20 | pub process_id: u32, 21 | #[serde(rename = "EnclaveCID")] 22 | /// The enclave's CID. 23 | pub enclave_cid: u64, 24 | #[serde(rename = "NumberOfCPUs")] 25 | /// The number of CPUs used by the enclave. 26 | pub cpu_count: u64, 27 | #[serde(rename = "CPUIDs")] 28 | /// The IDs of the CPUs used by the enclave. 29 | pub cpu_ids: Vec, 30 | #[serde(rename = "MemoryMiB")] 31 | /// The memory provided to the enclave (in MiB). 32 | pub memory_mib: u64, 33 | #[serde(rename = "State")] 34 | /// The current state of the enclave. 35 | pub state: String, 36 | #[serde(rename = "Flags")] 37 | /// The bit-mask which provides the enclave's launch flags. 38 | pub flags: String, 39 | } 40 | 41 | /// The information provided by a `run-enclave` request. 42 | #[derive(Clone, Serialize, Deserialize)] 43 | pub struct EnclaveRunInfo { 44 | #[serde(rename = "EnclaveID")] 45 | /// The full ID of the enclave. 46 | pub enclave_id: String, 47 | #[serde(rename = "ProcessID")] 48 | /// The PID of the enclave process which manages the enclave. 49 | pub process_id: u32, 50 | #[serde(rename = "EnclaveCID")] 51 | /// The enclave's CID. 52 | pub enclave_cid: u64, 53 | #[serde(rename = "NumberOfCPUs")] 54 | /// The number of CPUs used by the enclave. 55 | pub cpu_count: usize, 56 | #[serde(rename = "CPUIDs")] 57 | /// The IDs of the CPUs used by the enclave. 58 | pub cpu_ids: Vec, 59 | #[serde(rename = "MemoryMiB")] 60 | /// The memory provided to the enclave (in MiB). 61 | pub memory_mib: u64, 62 | } 63 | 64 | /// The information provided by a `terminate-enclave` request. 65 | #[derive(Clone, Serialize, Deserialize)] 66 | pub struct EnclaveTerminateInfo { 67 | #[serde(rename = "EnclaveID")] 68 | /// The full ID of the enclave. 69 | pub enclave_id: String, 70 | #[serde(rename = "Terminated")] 71 | /// A flag indicating if the enclave has terminated. 72 | pub terminated: bool, 73 | } 74 | 75 | fn parse_output(output: Output) -> Result { 76 | if !output.status.success() { 77 | return Err(format!( 78 | "{}, status code: {:?}", 79 | String::from_utf8_lossy(output.stderr.as_slice()), 80 | output.status.code(), 81 | )); 82 | } 83 | serde_json::from_slice(output.stdout.as_slice()) 84 | .map_err(|_| "command invalid output".to_string()) 85 | } 86 | 87 | fn run_enclave_daemon( 88 | image_path: &str, 89 | cpu_count: usize, 90 | memory_mib: u64, 91 | cid: Option, 92 | ) -> Result { 93 | let mut cmd = Command::new("nitro-cli"); 94 | cmd.arg("run-enclave") 95 | .args(["--eif-path", image_path]) 96 | .args(["--cpu-count", &format!("{}", cpu_count)]) 97 | .args(["--memory", &format!("{}", memory_mib)]); 98 | if let Some(cid) = cid { 99 | cmd.args(["--cid", &cid.to_string()]); 100 | } 101 | let output = cmd 102 | .output() 103 | .map_err(|e| format!("execute nitro-cli error: {}", e))?; 104 | parse_output(output) 105 | } 106 | 107 | /// start the enclave 108 | /// opt: the config to start enclave 109 | /// stop_receiver: when receiver data, the enclave will be stopped 110 | pub fn run_enclave(opt: &EnclaveOpt, stop_receiver: Receiver<()>) -> Result<(), String> { 111 | // check if the enclave already running 112 | let enclave_info = describe_enclave()?; 113 | if !enclave_info.is_empty() { 114 | let info = serde_json::to_string_pretty(&enclave_info).expect("get invalid enclave info"); 115 | return Err(format!( 116 | "the following enclave is already active, please stop and try again:\n{:?}", 117 | info 118 | )); 119 | } 120 | // lauch enclave server 121 | tracing::info!("start enclave log server at port {}", opt.log_server_port); 122 | let enclave_log_server = LogServer::new(opt.log_server_port).map_err(|e| format!("{:?}", e))?; 123 | 124 | enclave_log_server.launch(); 125 | // run enclave 126 | let info = run_enclave_daemon( 127 | &opt.eif_path, 128 | opt.cpu_count, 129 | opt.memory_mib, 130 | opt.enclave_cid, 131 | )?; 132 | let s = serde_json::to_string_pretty(&info).unwrap(); 133 | tracing::info!("run enclave success:\n{}", s); 134 | // waiting for stop signal and stop the enclave 135 | let _ = stop_receiver.recv(); 136 | let _ = stop_enclave(Some(info.enclave_id)); 137 | Ok(()) 138 | } 139 | 140 | /// stop enclave: if cid is None, stop all enclave 141 | pub fn stop_enclave(cid: Option) -> Result { 142 | let mut cmd = Command::new("nitro-cli"); 143 | cmd.arg("terminate-enclave"); 144 | if let Some(id) = cid { 145 | cmd.args(["--enclave-id", &id]); 146 | } else { 147 | cmd.arg("--all"); 148 | } 149 | let output = cmd 150 | .output() 151 | .map_err(|e| format!("execute nitro-cli error: {:?}", e))?; 152 | parse_output(output) 153 | } 154 | 155 | /// get all the enclave info 156 | pub fn describe_enclave() -> Result, String> { 157 | let output = Command::new("nitro-cli") 158 | .arg("describe-enclaves") 159 | .output() 160 | .map_err(|e| format!("execute nitro-cli error: {:?}", e))?; 161 | parse_output(output) 162 | } 163 | 164 | /// start vsock proxy 165 | /// opt: the config to start the proxy 166 | /// stop_receiver: when receive a data, the vsock proxy will exit 167 | pub fn run_vsock_proxy(opt: &VSockProxyOpt, stop_receiver: Receiver<()>) -> Result<(), String> { 168 | tracing::debug!("run vsock proxy with config: {:?}", opt); 169 | if check_vsock_proxy() { 170 | tracing::warn!("vsock proxy is already running, ignore this start"); 171 | return Ok(()); 172 | } 173 | let mut child = Command::new("vsock-proxy") 174 | .args(["--num_workers", &format!("{}", opt.num_workers)]) 175 | .args(["--config", &opt.config_file]) 176 | .arg(opt.local_port.to_string()) 177 | .arg(&opt.remote_addr) 178 | .arg(opt.remote_port.to_string()) 179 | .spawn() 180 | .map_err(|e| format!("spawn vsock proxy error: {:?}", e))?; 181 | 182 | // waiting for the stop signal 183 | let _ = stop_receiver.recv(); 184 | if child.kill().is_ok() { 185 | tracing::info!("vsock proxy stopped"); 186 | } 187 | Ok(()) 188 | } 189 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::AwsCredentials; 2 | use clap::Parser; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fs; 5 | use std::{convert::TryFrom, path::PathBuf}; 6 | use tendermint::chain; 7 | use tendermint_config::net; 8 | 9 | /// nitro options for toml configuration 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | #[serde(deny_unknown_fields)] 12 | pub struct NitroSignOpt { 13 | /// Address of the validator (`tcp://` or `unix://`) 14 | pub address: net::Address, 15 | /// Chain ID of the Tendermint network this validator is part of 16 | pub chain_id: chain::Id, 17 | /// Height at which to stop signing 18 | pub max_height: Option, 19 | /// Path to a file containing a cryptographic key 20 | pub sealed_consensus_key_path: PathBuf, 21 | /// Path to our Ed25519 identity key (if applicable) 22 | pub sealed_id_key_path: Option, 23 | /// Path to chain-specific `priv_validator_state.json` file 24 | pub state_file_path: PathBuf, 25 | /// Vsock cid to push config to 26 | pub enclave_config_cid: u32, 27 | /// Vsock port to push config to 28 | pub enclave_config_port: u32, 29 | /// Vsock port to listen on for state synchronization 30 | pub enclave_state_port: u32, 31 | /// Vsock port to forward privval plain traffic to TM over UDS (or just pass to enclave if TCP/secret connection) 32 | pub enclave_tendermint_conn: u32, 33 | /// AWS credentials -- if not set, they'll be obtained from IAM 34 | pub credentials: Option, 35 | /// AWS region 36 | pub aws_region: String, 37 | } 38 | 39 | impl NitroSignOpt { 40 | pub fn from_file(config_path: PathBuf) -> Result { 41 | let toml_string = std::fs::read_to_string(config_path) 42 | .map_err(|e| format!("toml config file failed to read: {:?}", e))?; 43 | toml::from_str(&toml_string) 44 | .map_err(|e| format!("toml config file failed to parse: {:?}", e)) 45 | } 46 | } 47 | 48 | #[derive(Parser, Clone, Serialize, Deserialize, Debug)] 49 | pub struct VSockProxyOpt { 50 | /// "Set the maximum number of simultaneous connections supported." 51 | #[arg(long, short = 'w', default_value = "4")] 52 | pub num_workers: usize, 53 | /// "Local vsock port to listen for incoming connections." 54 | #[arg(long, default_value = "8000")] 55 | pub local_port: u32, 56 | /// "Remote TCP port of the server to be proxyed." 57 | #[arg(long, default_value = "443")] 58 | pub remote_port: u16, 59 | /// "Address of the server to be proxyed." 60 | #[arg(long)] 61 | pub remote_addr: String, 62 | /// "YAML file containing the services that can be forwarded.\n" 63 | #[arg(long, default_value = "/etc/nitro_enclaves/vsock-proxy.yaml")] 64 | pub config_file: String, 65 | } 66 | 67 | impl Default for VSockProxyOpt { 68 | fn default() -> Self { 69 | Self { 70 | num_workers: 4, 71 | local_port: 8000, 72 | remote_port: 443, 73 | remote_addr: "kms.ap-southeast-1.amazonaws.com".to_string(), 74 | config_file: "/etc/nitro_enclaves/vsock-proxy.yaml".to_string(), 75 | } 76 | } 77 | } 78 | 79 | #[derive(Parser, Clone, Serialize, Deserialize, Debug)] 80 | pub struct EnclaveOpt { 81 | /// The path to the enclave image file 82 | #[arg(long, short = 'p', default_value = "/home/ec2-user/.tmkms/tmkms.eif")] 83 | pub eif_path: String, 84 | /// The optional enclave CID 85 | #[arg(long, short = 'i')] 86 | pub enclave_cid: Option, 87 | /// The amount of memory that will be given to the enclave. 88 | #[arg(long, default_value = "512")] 89 | pub memory_mib: u64, 90 | /// The number of CPUs that the enclave will receive. 91 | #[arg(long, conflicts_with("cpu_ids"))] 92 | pub cpu_count: usize, 93 | /// Set the enclave log server port 94 | #[arg(long, default_value = "6050")] 95 | pub log_server_port: u32, 96 | } 97 | 98 | impl Default for EnclaveOpt { 99 | fn default() -> Self { 100 | Self { 101 | eif_path: "tmkms.eif".to_string(), 102 | enclave_cid: None, 103 | memory_mib: 512, 104 | cpu_count: 2, 105 | log_server_port: 6050, 106 | } 107 | } 108 | } 109 | 110 | impl Default for NitroSignOpt { 111 | fn default() -> Self { 112 | Self { 113 | address: net::Address::Unix { 114 | path: "/tmp/validator.socket".into(), 115 | }, 116 | chain_id: chain::Id::try_from("testchain-1".to_owned()).expect("valid chain-id"), 117 | max_height: None, 118 | sealed_consensus_key_path: "secrets/secret.key".into(), 119 | sealed_id_key_path: Some("secrets/id.key".into()), 120 | state_file_path: "state/priv_validator_state.json".into(), 121 | enclave_config_cid: 15, 122 | enclave_config_port: 5050, 123 | enclave_state_port: 5555, 124 | enclave_tendermint_conn: 5000, 125 | credentials: None, 126 | aws_region: "ap-southeast-1".to_owned(), 127 | } 128 | } 129 | } 130 | 131 | /// the config to run the enclave and vsock proxy 132 | #[derive(Serialize, Deserialize, Default, Debug)] 133 | pub struct EnclaveConfig { 134 | pub vsock_proxy: VSockProxyOpt, 135 | pub enclave: EnclaveOpt, 136 | } 137 | 138 | impl EnclaveConfig { 139 | pub fn from_file(config_path: PathBuf) -> Result { 140 | if !config_path.exists() { 141 | return Err("config path is not exists".to_string()); 142 | } 143 | let toml_string = fs::read_to_string(config_path) 144 | .map_err(|e| format!("toml config file failed to read: {:?}", e))?; 145 | toml::from_str(&toml_string) 146 | .map_err(|e| format!("toml config file failed to parse: {:?}", e)) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/enclave_log_server.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::VSOCK_HOST_CID; 2 | use std::io::Read; 3 | use std::thread; 4 | use std::time::Duration; 5 | use tmkms_nitro_helper::tracing_layer::Log; 6 | use tracing::Level; 7 | use tracing::{debug, error, info, trace, warn}; 8 | use vsock::{VsockAddr, VsockListener}; 9 | 10 | /// Configuration parameters for port listening and remote destination 11 | pub struct LogServer { 12 | cid: u32, 13 | local_port: u32, 14 | } 15 | 16 | impl LogServer { 17 | pub fn new(local_port: u32) -> std::io::Result { 18 | Ok(Self { 19 | cid: VSOCK_HOST_CID, 20 | local_port, 21 | }) 22 | } 23 | 24 | /// Creates a listening socket 25 | /// Returns the file descriptor for it or the appropriate error 26 | fn sock_listen(&self) -> Result { 27 | info!( 28 | "binding enclave log server to vsock port: {}", 29 | self.local_port 30 | ); 31 | let sockaddr = VsockAddr::new(self.cid, self.local_port); 32 | let listener = VsockListener::bind(&sockaddr) 33 | .map_err(|e| format!("Could not bind to {:?}, {:?}", sockaddr, e))?; 34 | info!("Bound enclave log server to {:?}", sockaddr); 35 | Ok(listener) 36 | } 37 | 38 | /// keep listening 39 | pub fn launch(mut self) { 40 | thread::spawn(move || loop { 41 | match self.sock_listen() { 42 | Ok(listener) => { 43 | if let Err(e) = self.sock_accept(&listener) { 44 | error!("enclave log server connection failed {}", e); 45 | thread::sleep(Duration::new(1, 0)); 46 | } 47 | } 48 | Err(e) => { 49 | error!("enclave log server listening failed {}", e); 50 | thread::sleep(Duration::new(1, 0)); 51 | } 52 | } 53 | }); 54 | } 55 | 56 | /// Accepts an incoming connection coming on listener and handles it on a 57 | /// different thread 58 | /// Returns the handle for the new thread or the appropriate error 59 | fn sock_accept(&mut self, listener: &VsockListener) -> Result<(), String> { 60 | loop { 61 | let (mut client, client_addr) = listener 62 | .accept() 63 | .map_err(|_| "Enclave log server could not accept connection")?; 64 | trace!("Accepted connection on {:?}", client_addr); 65 | 66 | let mut tmp_buffer = [0u8; 8192]; 67 | let nbytes = client 68 | .read(&mut tmp_buffer) 69 | .map_err(|e| format!("{:?}", e))?; 70 | self.process_log(&tmp_buffer[0..nbytes])?; 71 | } 72 | } 73 | 74 | fn process_log(&mut self, raw_log: &[u8]) -> Result<(), String> { 75 | let log = Log::from_raw(raw_log).map_err(|e| format!("{:?}", e))?; 76 | let s = log.format(); 77 | match log.level { 78 | Level::TRACE => trace!("{}", s), 79 | Level::DEBUG => debug!("{}", s), 80 | Level::INFO => info!("{}", s), 81 | Level::WARN => warn!("{}", s), 82 | Level::ERROR => error!("{}", s), 83 | } 84 | Ok(()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/key_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::AwsCredentials; 2 | use crate::shared::{NitroKeygenConfig, NitroKeygenResponse, NitroRequest, NitroResponse}; 3 | 4 | use ed25519_consensus::VerificationKey; 5 | use std::{fs::OpenOptions, io::Write, os::unix::fs::OpenOptionsExt, path::Path}; 6 | use tmkms_light::utils::{read_u16_payload, write_u16_payload}; 7 | use vsock::VsockAddr; 8 | 9 | pub(crate) mod credential { 10 | use crate::shared::AwsCredentials; 11 | use aws_config::imds::credentials; 12 | use aws_credential_types::provider::ProvideCredentials; 13 | use tokio::runtime::Builder; 14 | 15 | /// get credentials from Aws Instance Metadata Service Version 2 16 | /// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html 17 | pub fn get_credentials() -> Result { 18 | let client = credentials::ImdsCredentialsProvider::builder().build(); 19 | let rt = Builder::new_current_thread() 20 | .enable_all() 21 | .build() 22 | .map_err(|e| format!("failed to create tokio runtime: {:?}", e))?; 23 | let aws_credential = rt 24 | .block_on(client.provide_credentials()) 25 | .map_err(|e| format!("invalid credential: {:?}", e))?; 26 | let credentials = AwsCredentials { 27 | aws_key_id: aws_credential.access_key_id().into(), 28 | aws_secret_key: aws_credential.secret_access_key().into(), 29 | aws_session_token: aws_credential.session_token().unwrap().into(), 30 | }; 31 | 32 | Ok(credentials) 33 | } 34 | } 35 | 36 | /// Generates a keypair and encrypts with AWS KMS at the given path 37 | /// and returns the public key with attestation doc for it and 38 | /// the used AWS KMS key id 39 | pub fn generate_key( 40 | cid: u32, 41 | port: u32, 42 | path: impl AsRef, 43 | region: &str, 44 | credentials: AwsCredentials, 45 | kms_key_id: String, 46 | ) -> Result<(VerificationKey, Vec), String> { 47 | let keygen_request = NitroKeygenConfig { 48 | credentials, 49 | kms_key_id, 50 | aws_region: region.into(), 51 | }; 52 | 53 | let request = NitroRequest::Keygen(keygen_request); 54 | let addr = VsockAddr::new(cid, port); 55 | let mut socket = vsock::VsockStream::connect(&addr).map_err(|e| { 56 | format!( 57 | "failed to connect to the enclave to generate key pair: {:?}", 58 | e 59 | ) 60 | })?; 61 | let request_raw = serde_json::to_vec(&request) 62 | .map_err(|e| format!("failed to serialize the keygen request: {:?}", e))?; 63 | write_u16_payload(&mut socket, &request_raw) 64 | .map_err(|e| format!("failed to write the config: {:?}", e))?; 65 | // get the response 66 | let json_raw = 67 | read_u16_payload(&mut socket).map_err(|_e| "failed to read config".to_string())?; 68 | let response: NitroResponse = serde_json::from_slice(&json_raw) 69 | .map_err(|e| format!("failed to get keygen response from enclave: {:?}", e))?; 70 | 71 | let resp: NitroKeygenResponse = response?; 72 | OpenOptions::new() 73 | .create(true) 74 | .write(true) 75 | .truncate(true) 76 | .mode(0o600) 77 | .open(path.as_ref()) 78 | .and_then(|mut file| file.write_all(&resp.encrypted_secret)) 79 | .map_err(|e| format!("couldn't write `{}`: {}", path.as_ref().display(), e))?; 80 | Ok(( 81 | VerificationKey::try_from(resp.public_key.as_slice()) 82 | .map_err(|e| format!("Invalid pubkey key: {:?}", e))?, 83 | resp.attestation_doc, 84 | )) 85 | } 86 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use shared::*; 2 | 3 | pub mod shared; 4 | pub mod tracing_layer; 5 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/main.rs: -------------------------------------------------------------------------------- 1 | mod command; 2 | mod config; 3 | mod enclave_log_server; 4 | mod key_utils; 5 | mod proxy; 6 | mod shared; 7 | mod state; 8 | 9 | use command::launch_all::launch_all; 10 | use command::nitro_enclave::{describe_enclave, run_enclave, stop_enclave}; 11 | use command::{check_vsock_proxy, init, start}; 12 | use config::{EnclaveOpt, VSockProxyOpt}; 13 | 14 | use crate::command::nitro_enclave::run_vsock_proxy; 15 | use crate::config::{EnclaveConfig, NitroSignOpt}; 16 | use clap::Parser; 17 | use std::path::PathBuf; 18 | use std::sync::mpsc::channel; 19 | use tmkms_light::utils::PubkeyDisplay; 20 | use tracing::Level; 21 | use tracing_subscriber::FmtSubscriber; 22 | 23 | /// Helper sub-commands 24 | #[derive(Debug, Parser)] 25 | #[command( 26 | name = "tmkms-nitro-helper", 27 | about = "helper (proxies etc.) for nitro enclave execution" 28 | )] 29 | enum TmkmsLight { 30 | #[command(flatten)] 31 | Helper(CommandHelper), 32 | #[command(subcommand)] 33 | Enclave(CommandEnclave), 34 | } 35 | 36 | /// enclave sub-commands 37 | #[derive(Debug, Parser)] 38 | enum CommandEnclave { 39 | #[command(name = "info", about = "get tmkms info")] 40 | Info, 41 | #[command(name = "run", about = "run enclave")] 42 | RunEnclave { 43 | #[command(flatten)] 44 | opt: EnclaveOpt, 45 | /// log level, default: info, -v: info, -vv: debug, -vvv: trace 46 | #[arg(short, action = clap::ArgAction::Count)] 47 | v: u32, 48 | }, 49 | #[command(name = "stop", about = "stop enclave")] 50 | StopEnclave { 51 | /// Stop the enclave cid 52 | #[arg(long)] 53 | cid: Option, 54 | }, 55 | #[command(name = "vsock-proxy", about = "launch vsock proxy")] 56 | RunProxy { 57 | #[command(flatten)] 58 | opt: VSockProxyOpt, 59 | /// log level, default: info, -v: info, -vv: debug, -vvv: trace 60 | #[arg(short, action = clap::ArgAction::Count)] 61 | v: u32, 62 | }, 63 | } 64 | 65 | /// Helper sub-commands 66 | #[derive(Debug, Parser)] 67 | enum CommandHelper { 68 | #[command(name = "init", about = "Create config + keygen")] 69 | /// Create config + keygen 70 | Init { 71 | /// the directory put the generated config files 72 | #[arg(short, default_value = "./")] 73 | config_dir: PathBuf, 74 | #[arg(short)] 75 | pubkey_display: Option, 76 | #[arg(short)] 77 | bech32_prefix: Option, 78 | #[arg(short)] 79 | aws_region: String, 80 | #[arg(short)] 81 | kms_key_id: String, 82 | #[arg(long)] 83 | cid: Option, 84 | }, 85 | #[command(name = "start", about = "start tmkms process")] 86 | /// start tmkms process (push config + start up proxy and state persistence) 87 | Start { 88 | #[arg(short, default_value = "tmkms.toml")] 89 | config_path: PathBuf, 90 | #[arg(long)] 91 | cid: Option, 92 | /// log level, default: info, -v: info, -vv: debug, -vvv: trace 93 | #[arg(short, action = clap::ArgAction::Count)] 94 | v: u32, 95 | }, 96 | #[command(name = "launch-all", about = "launch all")] 97 | LaunchAll { 98 | /// tmkms config path 99 | #[arg(short, default_value = "tmkms.toml")] 100 | tmkms_config: PathBuf, 101 | /// enclave config path 102 | #[arg(short, default_value = "enclave.toml")] 103 | enclave_config: PathBuf, 104 | /// log level, default: info, -v: info, -vv: debug, -vvv: trace 105 | #[arg(short, action = clap::ArgAction::Count)] 106 | v: u32, 107 | }, 108 | } 109 | 110 | fn set_logger(v: u32) -> Result<(), String> { 111 | let log_level = match v { 112 | 0 | 1 => Level::INFO, 113 | 2 => Level::DEBUG, 114 | _ => Level::TRACE, 115 | }; 116 | let subscriber = FmtSubscriber::builder().with_max_level(log_level).finish(); 117 | tracing::subscriber::set_global_default(subscriber) 118 | .map_err(|e| format!("setting default subscriber failed: {:?}", e))?; 119 | Ok(()) 120 | } 121 | 122 | fn run() -> Result<(), String> { 123 | let opt = TmkmsLight::parse(); 124 | match opt { 125 | TmkmsLight::Helper(CommandHelper::Init { 126 | config_dir, 127 | pubkey_display, 128 | bech32_prefix, 129 | aws_region, 130 | kms_key_id, 131 | cid, 132 | }) => { 133 | init( 134 | config_dir, 135 | pubkey_display, 136 | bech32_prefix, 137 | aws_region, 138 | kms_key_id, 139 | cid, 140 | )?; 141 | } 142 | TmkmsLight::Helper(CommandHelper::Start { 143 | config_path, 144 | cid, 145 | v, 146 | }) => { 147 | set_logger(v)?; 148 | let config = NitroSignOpt::from_file(config_path)?; 149 | if !check_vsock_proxy() { 150 | return Err("vsock proxy not started".to_string()); 151 | } 152 | let (sender, receiver) = channel(); 153 | ctrlc::set_handler(move || { 154 | let _ = sender.send(()); 155 | }) 156 | .map_err(|_| "Error to set Ctrl-C channel".to_string())?; 157 | start(&config, cid, receiver)?; 158 | } 159 | TmkmsLight::Enclave(CommandEnclave::Info) => { 160 | let info = describe_enclave()?; 161 | let s = serde_json::to_string_pretty(&info) 162 | .map_err(|_| "get invalid enclave info".to_string())?; 163 | println!("enclave status:\n{}", s); 164 | } 165 | TmkmsLight::Enclave(CommandEnclave::RunEnclave { opt, v }) => { 166 | set_logger(v)?; 167 | let (sender, receiver) = channel(); 168 | ctrlc::set_handler(move || { 169 | let _ = sender.send(()); 170 | }) 171 | .map_err(|_| "Error to set Ctrl-C channel".to_string())?; 172 | run_enclave(&opt, receiver)?; 173 | } 174 | TmkmsLight::Enclave(CommandEnclave::StopEnclave { cid }) => { 175 | stop_enclave(cid)?; 176 | } 177 | TmkmsLight::Enclave(CommandEnclave::RunProxy { opt, v }) => { 178 | set_logger(v)?; 179 | let (sender, receiver) = channel(); 180 | ctrlc::set_handler(move || { 181 | let _ = sender.send(()); 182 | }) 183 | .map_err(|_| "Error to set Ctrl-C channel".to_string())?; 184 | run_vsock_proxy(&opt, receiver)?; 185 | } 186 | TmkmsLight::Helper(CommandHelper::LaunchAll { 187 | tmkms_config, 188 | enclave_config, 189 | v, 190 | }) => { 191 | set_logger(v)?; 192 | let tmkms_config = NitroSignOpt::from_file(tmkms_config)?; 193 | let enclave_config = EnclaveConfig::from_file(enclave_config)?; 194 | launch_all(tmkms_config, enclave_config)?; 195 | } 196 | }; 197 | Ok(()) 198 | } 199 | 200 | fn main() { 201 | if let Err(e) = run() { 202 | eprintln!("{}", e); 203 | std::process::exit(1); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/proxy.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::VSOCK_HOST_CID; 2 | use nix::sys::select::{select, FdSet}; 3 | use std::io::Read; 4 | use std::io::Write; 5 | use std::os::unix::io::AsRawFd; 6 | use std::os::unix::net::UnixStream; 7 | use std::path::PathBuf; 8 | use std::thread; 9 | use std::time::Duration; 10 | use tracing::{error, info, trace}; 11 | use vsock::{VsockAddr, VsockListener}; 12 | 13 | /// Configuration parameters for port listening and remote destination 14 | pub struct Proxy { 15 | local_port: u32, 16 | remote_addr: PathBuf, 17 | } 18 | 19 | impl Proxy { 20 | /// creates a new vsock<->uds proxy 21 | pub fn new(local_port: u32, remote_addr: PathBuf) -> Self { 22 | Self { 23 | local_port, 24 | remote_addr, 25 | } 26 | } 27 | 28 | /// Creates a listening socket 29 | /// Returns the file descriptor for it or the appropriate error 30 | pub fn sock_listen(&self) -> Result { 31 | info!("binding proxy to vsock port: {}", self.local_port); 32 | let sockaddr = VsockAddr::new(VSOCK_HOST_CID, self.local_port); 33 | let listener = VsockListener::bind(&sockaddr) 34 | .map_err(|_| format!("Could not bind to {:?}", sockaddr))?; 35 | info!("Bound to {:?}", sockaddr); 36 | Ok(listener) 37 | } 38 | 39 | /// Accepts an incoming connection coming on listener and handles it on a 40 | /// different thread 41 | /// Returns the handle for the new thread or the appropriate error 42 | pub fn sock_accept(&self, listener: &VsockListener) -> Result<(), String> { 43 | let (mut client, client_addr) = listener 44 | .accept() 45 | .map_err(|_| "Could not accept connection")?; 46 | info!("Accepted connection on {:?}", client_addr); 47 | let mut server = UnixStream::connect(&self.remote_addr) 48 | .map_err(|_| format!("Could not connect to {:?}", self.remote_addr))?; 49 | 50 | let client_socket = client.as_raw_fd(); 51 | let server_socket = server.as_raw_fd(); 52 | 53 | let mut disconnected = false; 54 | while !disconnected { 55 | let mut set = FdSet::new(); 56 | set.insert(client_socket); 57 | set.insert(server_socket); 58 | trace!("proxy peer addr: {:?}", client.peer_addr()); 59 | trace!("proxy local addr: {:?}", client.local_addr()); 60 | trace!("proxy fd: {} {}", client.as_raw_fd(), client_socket); 61 | trace!("proxy uds/server fd: {}", server_socket); 62 | select(None, Some(&mut set), None, None, None).expect("select"); 63 | 64 | trace!("client -> server"); 65 | if set.contains(client_socket) { 66 | disconnected = transfer(&mut client, &mut server); 67 | } 68 | trace!("server -> client"); 69 | if set.contains(server_socket) { 70 | disconnected = transfer(&mut server, &mut client); 71 | } 72 | } 73 | info!("Client on {:?} disconnected", client_addr); 74 | Ok(()) 75 | } 76 | 77 | /// keep listening / re-connecting 78 | pub fn launch_proxy(self) { 79 | thread::spawn(move || loop { 80 | match self.sock_listen() { 81 | Ok(listener) => { 82 | if let Err(e) = self.sock_accept(&listener) { 83 | error!("connection failed {}", e); 84 | thread::sleep(Duration::new(1, 0)); 85 | } 86 | } 87 | Err(e) => { 88 | error!("listening failed {}", e); 89 | thread::sleep(Duration::new(1, 0)); 90 | } 91 | } 92 | }); 93 | } 94 | } 95 | 96 | /// Transfers a chunck of maximum 8KB from src to dst 97 | /// If no error occurs, returns true if the source disconnects and false otherwise 98 | fn transfer(src: &mut dyn Read, dst: &mut dyn Write) -> bool { 99 | const BUFF_SIZE: usize = 8192; 100 | 101 | let mut buffer = [0u8; BUFF_SIZE]; 102 | 103 | let nbytes = src.read(&mut buffer); 104 | let nbytes = nbytes.unwrap_or(0); 105 | 106 | if nbytes == 0 { 107 | return true; 108 | } 109 | trace!("transfer data: {:02X?}", &buffer[..nbytes]); 110 | dst.write_all(&buffer[..nbytes]).is_err() 111 | } 112 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/shared.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use tendermint::{chain, node}; 3 | 4 | /// CID for listening on the host 5 | pub const VSOCK_HOST_CID: u32 = 3; 6 | 7 | /// Nitro config to be pushed to the enclave 8 | #[derive(Debug, Serialize, Deserialize)] 9 | #[serde(deny_unknown_fields)] 10 | pub struct NitroConfig { 11 | /// Chain ID of the Tendermint network this validator is part of 12 | pub chain_id: chain::Id, 13 | /// Height at which to stop signing 14 | pub max_height: Option, 15 | /// AWS KMS-encrypted key 16 | pub sealed_consensus_key: Vec, 17 | /// AWS KMS-encrypted Ed25519 identity key (if secret connection) 18 | pub sealed_id_key: Option>, 19 | /// peer id to check with secret connections 20 | pub peer_id: Option, 21 | /// Vsock port to listen on for state synchronization 22 | pub enclave_state_port: u32, 23 | /// Vsock port to forward privval plain traffic to TM over UDS or TCP 24 | pub enclave_tendermint_conn: u32, 25 | /// AWS credentials -- if not set, they'll be obtained from IAM 26 | pub credentials: AwsCredentials, 27 | /// AWS region 28 | pub aws_region: String, 29 | } 30 | 31 | /// configuration sent during key generation 32 | #[derive(Debug, Serialize, Deserialize)] 33 | pub struct NitroKeygenConfig { 34 | /// AWS credentials -- if not set, they'll be obtained from IAM 35 | pub credentials: AwsCredentials, 36 | /// AWS key id 37 | pub kms_key_id: String, 38 | /// AWS region 39 | pub aws_region: String, 40 | } 41 | 42 | /// types of initial requests sent to NE 43 | #[derive(Debug, Serialize, Deserialize)] 44 | pub enum NitroRequest { 45 | /// generate a key 46 | Keygen(NitroKeygenConfig), 47 | /// start up TMKMS processing 48 | Start(NitroConfig), 49 | } 50 | 51 | /// response from key generation 52 | #[derive(Debug, Serialize, Deserialize)] 53 | pub struct NitroKeygenResponse { 54 | /// payload returned from AWS KMS 55 | pub encrypted_secret: Vec, 56 | /// public key for consensus or P2P 57 | pub public_key: Vec, 58 | /// attestation payload (COSE_Sign1) for the public key + encryption key id 59 | pub attestation_doc: Vec, 60 | } 61 | 62 | /// response from the enclave 63 | pub type NitroResponse = Result; 64 | 65 | /// Credentials, generally obtained from parent instance IAM 66 | #[derive(Debug, Clone, Serialize, Deserialize)] 67 | #[serde(deny_unknown_fields)] 68 | pub struct AwsCredentials { 69 | /// AccessKeyId 70 | pub aws_key_id: String, 71 | /// SecretAccessKey 72 | pub aws_secret_key: String, 73 | /// SessionToken 74 | pub aws_session_token: String, 75 | } 76 | -------------------------------------------------------------------------------- /providers/nitro/nitro-helper/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::VSOCK_HOST_CID; 2 | use std::os::unix::io::AsRawFd; 3 | use std::sync::mpsc::{Receiver, TryRecvError}; 4 | use std::thread; 5 | use std::{ 6 | fs, 7 | io::{self, prelude::*}, 8 | path::{Path, PathBuf}, 9 | }; 10 | use tempfile::NamedTempFile; 11 | use tmkms_light::chain::state::{consensus, StateError}; 12 | use tmkms_light::error::{io_error_wrap, Error}; 13 | use tmkms_light::utils::{read_u16_payload, write_u16_payload}; 14 | use tracing::{debug, info, warn}; 15 | use vsock::{VsockAddr, VsockListener, VsockStream}; 16 | 17 | /// helps the enclave to load the state previously persisted on the host 18 | /// + to persist new states 19 | pub struct StateSyncer { 20 | state_file_path: PathBuf, 21 | vsock_listener: VsockListener, 22 | state: consensus::State, 23 | } 24 | 25 | impl StateSyncer { 26 | /// creates a new state file or loads the previous one 27 | /// and binds a listener for incoming vsock connections from the enclave 28 | /// on the proxy CID on the provided port 29 | pub fn new>(path: P, vsock_port: u32) -> Result { 30 | let state_file_path = path.as_ref().to_owned(); 31 | let state = match fs::read_to_string(&path) { 32 | Ok(state_json) => { 33 | let consensus_state: consensus::State = 34 | serde_json::from_str(&state_json).map_err(|e| { 35 | StateError::sync_enc_dec_error(path.as_ref().display().to_string(), e) 36 | })?; 37 | 38 | Ok(consensus_state) 39 | } 40 | Err(e) if e.kind() == io::ErrorKind::NotFound => { 41 | Self::write_initial_state(&state_file_path) 42 | } 43 | Err(e) => Err(StateError::sync_error( 44 | path.as_ref().display().to_string(), 45 | e, 46 | )), 47 | }?; 48 | 49 | let sockaddr = VsockAddr::new(VSOCK_HOST_CID, vsock_port); 50 | let vsock_listener = VsockListener::bind(&sockaddr) 51 | .map_err(|e| StateError::sync_error("vsock".into(), e))?; 52 | 53 | Ok(Self { 54 | state_file_path, 55 | vsock_listener, 56 | state, 57 | }) 58 | } 59 | 60 | /// Write the initial state to the given path on disk 61 | fn write_initial_state(path: &Path) -> Result { 62 | let consensus_state = consensus::State { 63 | height: 0u32.into(), 64 | ..Default::default() 65 | }; 66 | 67 | Self::persist_state(path, &consensus_state)?; 68 | 69 | Ok(consensus_state) 70 | } 71 | 72 | /// dump the current state to the provided vsock stream 73 | fn sync_to_stream(&self, stream: &mut VsockStream) -> Result<(), StateError> { 74 | let json_raw = serde_json::to_vec(&self.state) 75 | .map_err(|e| StateError::sync_enc_dec_error("vsock".into(), e))?; 76 | write_u16_payload(stream, &json_raw).map_err(|e| StateError::sync_error("vsock".into(), e)) 77 | } 78 | 79 | /// load state from the provided vsock stream 80 | fn sync_from_stream(mut stream: &mut VsockStream) -> Result { 81 | let json_raw = read_u16_payload(&mut stream)?; 82 | serde_json::from_slice(&json_raw).map_err(|e| io_error_wrap("parse error".into(), e)) 83 | } 84 | 85 | /// Launches the state syncer, when get data from stop_recv, the thread will be finished 86 | pub fn launch_syncer(mut self, stop_recv: Receiver<()>) -> thread::JoinHandle<()> { 87 | thread::spawn(move || { 88 | info!("listening for enclave persistence"); 89 | for conn in self.vsock_listener.incoming() { 90 | match conn { 91 | Ok(mut stream) => { 92 | info!("vsock persistence connection established"); 93 | debug!("state peer addr: {:?}", stream.peer_addr()); 94 | debug!("state local addr: {:?}", stream.local_addr()); 95 | debug!("state fd: {}", stream.as_raw_fd()); 96 | 97 | if let Err(e) = self.sync_to_stream(&mut stream) { 98 | warn!("error serializing to json {}", e); 99 | } else { 100 | loop { 101 | if let Ok(consensus_state) = Self::sync_from_stream(&mut stream) { 102 | self.state = consensus_state; 103 | if let Err(e) = 104 | Self::persist_state(&self.state_file_path, &self.state) 105 | { 106 | warn!("state persistence failed: {}", e); 107 | } 108 | match stop_recv.try_recv() { 109 | Ok(()) | Err(TryRecvError::Disconnected) => { 110 | warn!("stop state persistence"); 111 | return; 112 | } 113 | Err(TryRecvError::Empty) => continue, 114 | } 115 | } 116 | } 117 | } 118 | } 119 | Err(e) => { 120 | warn!("Vsock connection failed: {}", e); 121 | } 122 | } 123 | } 124 | }) 125 | } 126 | 127 | /// write the new state into a file on the host 128 | fn persist_state(path: &Path, new_state: &consensus::State) -> Result<(), StateError> { 129 | debug!( 130 | "writing new consensus state to {}: {:?}", 131 | path.display(), 132 | &new_state 133 | ); 134 | 135 | let json = serde_json::to_string(&new_state) 136 | .map_err(|e| StateError::sync_enc_dec_error(path.display().to_string(), e))?; 137 | 138 | let state_file_dir = path.parent().unwrap_or_else(|| { 139 | panic!("state file cannot be root directory"); 140 | }); 141 | 142 | let mut state_file = NamedTempFile::new_in(state_file_dir) 143 | .map_err(|e| StateError::sync_error(path.display().to_string(), e))?; 144 | state_file 145 | .write_all(json.as_bytes()) 146 | .map_err(|e| StateError::sync_error(path.display().to_string(), e))?; 147 | state_file 148 | .persist(path) 149 | .map_err(|e| StateError::sync_error(path.display().to_string(), e.error))?; 150 | 151 | debug!( 152 | "successfully wrote new consensus state to {}", 153 | path.display(), 154 | ); 155 | 156 | Ok(()) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /providers/sgx/sgx-app/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-fortanix-unknown-sgx" -------------------------------------------------------------------------------- /providers/sgx/sgx-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmkms-light-sgx-app" 3 | version = "0.4.2" 4 | authors = ["Tomas Tauber <2410580+tomtau@users.noreply.github.com>", "Linfeng Yuan "] 5 | edition = "2021" 6 | 7 | [target.'cfg(target_env = "sgx")'.dependencies] 8 | aes = "0.8" 9 | aes-gcm-siv = "0.11" 10 | base64 = "0.21" 11 | ed25519-consensus = "2" 12 | flex-error = "0.4" 13 | rand = "0.8" 14 | rsa = "0.7" 15 | serde_json = "1" 16 | sha2 = "0.10" 17 | sgx-isa = { version = "0.4", features = ["sgxstd"] } 18 | subtle = "2" 19 | subtle-encoding = "0.5" 20 | tendermint-p2p = "0.30" 21 | tmkms-light-sgx-runner = { path = "../sgx-runner" } 22 | tmkms-light = { path = "../../.." } 23 | tracing = "0.1" 24 | tracing-subscriber = "0.3" 25 | zeroize = "1" 26 | 27 | [dev-dependencies] 28 | quickcheck = "1" 29 | quickcheck_macros = "1" 30 | -------------------------------------------------------------------------------- /providers/sgx/sgx-app/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_env = "sgx")] 2 | mod sgx_app; 3 | 4 | #[cfg(target_env = "sgx")] 5 | fn main() -> std::io::Result<()> { 6 | let mut args = std::env::args(); 7 | // enclave-runner had a breaking change that it started to pass the enclave path 8 | if args.len() > 2 { 9 | let _path = args.next(); 10 | } 11 | let command = args.next(); 12 | let log_level = match args.next() { 13 | Some(s) if s == "verbose" => tracing::Level::DEBUG, 14 | _ => tracing::Level::INFO, 15 | }; 16 | let subscriber = tracing_subscriber::FmtSubscriber::builder() 17 | .with_max_level(log_level) 18 | .finish(); 19 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 20 | 21 | if command.is_none() { 22 | tracing::error!("no enclave command provided"); 23 | return Err(std::io::ErrorKind::Other.into()); 24 | } 25 | 26 | let request = serde_json::from_str(&command.unwrap()); 27 | if request.is_err() { 28 | tracing::error!("invalid enclave command provided"); 29 | return Err(std::io::ErrorKind::Other.into()); 30 | } 31 | let request = request.unwrap(); 32 | // "init" stream is provided by the enclave runner 33 | // user call extension (in sgx-runner) 34 | let init_conn = std::net::TcpStream::connect("init")?; 35 | tracing::info!("connected to init stream"); 36 | if let Err(e) = sgx_app::entry(init_conn, request) { 37 | tracing::error!("error: {}", e); 38 | Err(e) 39 | } else { 40 | Ok(()) 41 | } 42 | } 43 | 44 | #[cfg(not(target_env = "sgx"))] 45 | fn main() { 46 | eprintln!("`tmkms-light-sgx-app` should be compiled for `x86_64-fortanix-unknown-sgx` target"); 47 | } 48 | -------------------------------------------------------------------------------- /providers/sgx/sgx-app/src/sgx_app/cloud/keywrap.rs: -------------------------------------------------------------------------------- 1 | use aes::cipher::{BlockDecrypt, KeyInit}; 2 | use aes::{ 3 | cipher::generic_array::{ 4 | typenum::{U32, U40}, 5 | GenericArray, 6 | }, 7 | Aes256, 8 | }; 9 | use std::convert::TryInto; 10 | use subtle::ConstantTimeEq; 11 | use zeroize::Zeroize; 12 | 13 | /// helper for `aes256_unwrap_key_with_pad` 14 | fn aes256_unwrap_key_and_iv( 15 | kek: &GenericArray, 16 | input: &GenericArray, 17 | ) -> (GenericArray, [u8; 8]) { 18 | // https://www.ietf.org/rfc/rfc3394.txt 19 | // Section 2.2.2 step 1) init 20 | let n = input.len() / 8 - 1; 21 | 22 | let mut r = vec![0u8; input.len()]; 23 | r[8..].copy_from_slice(&input[8..]); 24 | 25 | let cipher = Aes256::new(kek); 26 | // SAFETY: lengths are statically checked 27 | let mut a = u64::from_be_bytes(input[..8].try_into().unwrap()); 28 | // Section 2.2.2 step 2) intermediate values 29 | for j in (0..6).rev() { 30 | for i in (1..n + 1).rev() { 31 | let t = (n * j + i) as u64; 32 | let mut ciphertext = [0u8; 16]; 33 | let b_i = i * 8; 34 | ciphertext[..8].copy_from_slice(&(a ^ t).to_be_bytes()); 35 | ciphertext[8..].copy_from_slice(&r[b_i..][0..8]); 36 | let block = GenericArray::from_mut_slice(&mut ciphertext); 37 | cipher.decrypt_block(block); 38 | // SAFETY: static allocation above 39 | a = u64::from_be_bytes(ciphertext[..8].try_into().unwrap()); 40 | r[b_i..][0..8].copy_from_slice(&ciphertext[8..]); 41 | } 42 | } 43 | let key = GenericArray::clone_from_slice(&r[8..]); 44 | r.zeroize(); 45 | (key, a.to_be_bytes()) 46 | } 47 | 48 | /// Possible errors in unwrapping 49 | #[derive(Debug, PartialEq, Eq)] 50 | pub enum UnwrapError { 51 | /// key or payload incorrect length 52 | IncorrectLength, 53 | /// AIV mismmatch 54 | AuthFailed, 55 | } 56 | 57 | /// This unwraps the key as per RFC3394/RFC5649 58 | /// but hardcodes the length assumptions for kek to be 32-bytes 59 | /// and the unwrapped key to be 32-bytes (as that's what's provided 60 | /// in cloud environments). 61 | /// 62 | /// NOTE: RFC3394/RFC5649-compliant AES-KW / AES-KWP 63 | /// is implemented in the `aes-keywrap-rs` 64 | /// (`aes-keywrap` doesn't implement it: https://github.com/jedisct1/rust-aes-keywrap/issues/2), 65 | /// but 1) it depends on the `crypto2` crate which may not have received as much scrutiny 66 | /// as the `aes` crate (which is used everywhere else here), 67 | /// 2) we don't need most of its functionality. 68 | /// Hence, this custom partial implementation. 69 | pub fn aes256_unwrap_key_with_pad( 70 | kek: &[u8], 71 | wrapped: &[u8], 72 | ) -> Result, UnwrapError> { 73 | if kek.len() != 32 || wrapped.len() != 40 { 74 | return Err(UnwrapError::IncorrectLength); 75 | } 76 | let kek = GenericArray::from_slice(kek); 77 | let wrapped = GenericArray::from_slice(wrapped); 78 | let (mut key, key_iv) = aes256_unwrap_key_and_iv(kek, wrapped); 79 | 80 | // Alternate initial value for aes key wrapping, as defined in RFC 5649 section 3 81 | // http://www.ietf.org/rfc/rfc5649.txt 82 | // big-ending length hardcoded to 32 (as here, it's always unwrapping 32-byte sealing key) 83 | const IV_5649: [u8; 8] = [0xa6, 0x59, 0x59, 0xa6, 0, 0, 0, 32]; 84 | 85 | if IV_5649.ct_eq(&key_iv).unwrap_u8() == 0u8 { 86 | key.zeroize(); 87 | return Err(UnwrapError::AuthFailed); 88 | } 89 | // no need to remove padding, as the length is hardcoded to 32 90 | Ok(key) 91 | } 92 | 93 | #[cfg(test)] 94 | pub mod tests { 95 | use super::*; 96 | use aes::cipher::BlockEncrypt; 97 | use quickcheck::{Arbitrary, Gen}; 98 | use quickcheck_macros::quickcheck; 99 | 100 | /// quick test implementation based on https://www.ietf.org/rfc/rfc3394.txt 101 | fn aes256_wrap_key_and_iv( 102 | kek: &GenericArray, 103 | input: &GenericArray, 104 | iv: u64, 105 | ) -> GenericArray { 106 | let n = input.len() / 8 - 1; 107 | let mut r = vec![0u8; input.len() + 8]; 108 | r[..8].copy_from_slice(&u64::to_be_bytes(iv)); 109 | r[8..].copy_from_slice(&input); 110 | let cipher = Aes256::new(kek); 111 | let mut t = 0; 112 | for _j in 0..6 { 113 | for i in 1..=n + 1 { 114 | t += 1; 115 | let mut ciphertext = [0u8; 16]; 116 | let b_i = i * 8; 117 | ciphertext[..8].copy_from_slice(&r[..8]); 118 | ciphertext[8..].copy_from_slice(&r[b_i..][0..8]); 119 | let block = GenericArray::from_mut_slice(&mut ciphertext); 120 | cipher.encrypt_block(block); 121 | let a = u64::from_be_bytes(ciphertext[..8].try_into().unwrap()) ^ t; 122 | r[..8].copy_from_slice(&a.to_be_bytes()); 123 | r[b_i..][0..8].copy_from_slice(&ciphertext[8..]); 124 | } 125 | } 126 | 127 | let mut ret = GenericArray::default(); 128 | ret.copy_from_slice(&r); 129 | ret 130 | } 131 | 132 | /// quick test implementation with hardcoded http://www.ietf.org/rfc/rfc5649.txt 133 | pub fn aes256_wrap_key_with_pad(kek: &[u8], plaintext: &[u8]) -> GenericArray { 134 | const IV: u64 = u64::from_be_bytes([0xa6, 0x59, 0x59, 0xa6, 0, 0, 0, 32]); 135 | let key = GenericArray::from_slice(kek); 136 | let input = GenericArray::from_slice(plaintext); 137 | aes256_wrap_key_and_iv(key, input, IV) 138 | } 139 | 140 | #[derive(Clone, Copy, Debug)] 141 | struct KeyWrap(pub [u8; 32]); 142 | 143 | impl Arbitrary for KeyWrap { 144 | fn arbitrary(g: &mut Gen) -> Self { 145 | let mut raw = [0u8; 32]; 146 | for i in 0..raw.len() { 147 | let b = u8::arbitrary(g); 148 | raw[i] = b; 149 | } 150 | Self(raw) 151 | } 152 | } 153 | 154 | #[test] 155 | fn test_wrong_len() { 156 | assert_eq!( 157 | aes256_unwrap_key_with_pad(&[0u8; 10], &[0u8; 40]), 158 | Err(UnwrapError::IncorrectLength) 159 | ); 160 | assert_eq!( 161 | aes256_unwrap_key_with_pad(&[0u8; 32], &[0u8; 10]), 162 | Err(UnwrapError::IncorrectLength) 163 | ); 164 | } 165 | 166 | #[test] 167 | fn test_wrong_auth() { 168 | assert_eq!( 169 | aes256_unwrap_key_with_pad(&[0u8; 32], &[0u8; 40]), 170 | Err(UnwrapError::AuthFailed) 171 | ); 172 | } 173 | 174 | #[test] 175 | fn test_padded_256bit_kek_and_256bit_key() { 176 | let kek = subtle_encoding::hex::decode_upper( 177 | "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", 178 | ) 179 | .unwrap(); 180 | let cipher = subtle_encoding::hex::decode_upper( 181 | "4A8029243027353B0694CF1BD8FC745BB0CE8A739B19B1960B12426D4C39CFEDA926D103AB34E9F6", 182 | ) 183 | .unwrap(); 184 | let plain = subtle_encoding::hex::decode_upper( 185 | "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F", 186 | ) 187 | .unwrap(); 188 | assert_eq!(cipher, aes256_wrap_key_with_pad(&kek, &plain).to_vec()); 189 | assert_eq!( 190 | plain, 191 | aes256_unwrap_key_with_pad(&kek, &cipher).unwrap().to_vec() 192 | ); 193 | } 194 | 195 | #[quickcheck] 196 | fn wrap_unwrap(kek: KeyWrap, key: KeyWrap) -> bool { 197 | let wrapped = aes256_wrap_key_with_pad(&kek.0, &key.0); 198 | let unwrap = aes256_unwrap_key_with_pad(&kek.0, &wrapped).expect("unwrap"); 199 | let uref: &[u8] = unwrap.as_ref(); 200 | uref == &key.0 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /providers/sgx/sgx-app/src/sgx_app/keypair_seal.rs: -------------------------------------------------------------------------------- 1 | use aes_gcm_siv::{ 2 | aead::{generic_array::GenericArray, Aead, Payload}, 3 | Aes128GcmSiv, KeyInit, 4 | }; 5 | use ed25519_consensus::{SigningKey, VerificationKey}; 6 | use rand::{rngs::OsRng, RngCore}; 7 | use sgx_isa::{ErrorCode, Keyname, Keypolicy, Keyrequest, Report}; 8 | use std::convert::TryInto; 9 | use subtle::ConstantTimeEq; 10 | use tmkms_light_sgx_runner::SealedKeyData; 11 | use zeroize::Zeroize; 12 | 13 | fn seal_payload( 14 | csprng: &mut OsRng, 15 | payload: Payload, 16 | keyid: [u8; 32], 17 | ) -> Result { 18 | let mut nonce = [0u8; 12]; 19 | csprng.fill_bytes(&mut nonce); 20 | let report = Report::for_self(); 21 | let key_request = Keyrequest { 22 | keyname: Keyname::Seal as _, 23 | keypolicy: Keypolicy::MRSIGNER, 24 | isvsvn: report.isvsvn, 25 | cpusvn: report.cpusvn, 26 | keyid, 27 | ..Default::default() 28 | }; 29 | let nonce_ga = GenericArray::from_slice(&nonce); 30 | let mut key = key_request.egetkey()?; 31 | let gk = GenericArray::from_slice(&key); 32 | let aead = Aes128GcmSiv::new(gk); 33 | if let Ok(sealed_secret) = aead.encrypt(nonce_ga, payload) { 34 | key.zeroize(); 35 | Ok(SealedKeyData { 36 | seal_key_request: key_request.into(), 37 | nonce, 38 | sealed_secret, 39 | }) 40 | } else { 41 | key.zeroize(); 42 | Err(ErrorCode::MacCompareFail) 43 | } 44 | } 45 | 46 | /// Seals the provided ed25519 keypair with `Aes128GcmSiv` 47 | /// via a key request against MRSIGNER (so that versions with higher `isvsvn` 48 | /// can unseal the keypair) 49 | pub fn seal(csprng: &mut OsRng, keypair: &SigningKey) -> Result { 50 | let pubkey = keypair.verification_key(); 51 | let payload = Payload { 52 | msg: keypair.as_bytes(), 53 | aad: pubkey.as_bytes(), 54 | }; 55 | seal_payload(csprng, payload, pubkey.to_bytes()) 56 | } 57 | 58 | pub fn seal_secret( 59 | csprng: &mut OsRng, 60 | secret: &[u8], 61 | keyid: [u8; 32], 62 | ) -> Result { 63 | let payload = Payload { 64 | msg: secret, 65 | aad: &keyid, 66 | }; 67 | seal_payload(csprng, payload, keyid) 68 | } 69 | 70 | pub fn unseal_secret(sealed_data: &SealedKeyData) -> Result, ErrorCode> { 71 | if sealed_data.seal_key_request.keyname != (Keyname::Seal as u16) { 72 | return Err(ErrorCode::InvalidKeyname); 73 | } 74 | let key_request: Keyrequest = sealed_data 75 | .seal_key_request 76 | .try_into() 77 | .map_err(|_| ErrorCode::InvalidAttribute)?; 78 | let payload = Payload { 79 | msg: &sealed_data.sealed_secret, 80 | aad: &sealed_data.seal_key_request.keyid, 81 | }; 82 | let nonce_ga = GenericArray::from_slice(&sealed_data.nonce); 83 | let mut key = key_request.egetkey()?; 84 | let gk = GenericArray::from_slice(&key); 85 | let aead = Aes128GcmSiv::new(gk); 86 | if let Ok(secret_key) = aead.decrypt(nonce_ga, payload) { 87 | key.zeroize(); 88 | Ok(secret_key) 89 | } else { 90 | key.zeroize(); 91 | Err(ErrorCode::MacCompareFail) 92 | } 93 | } 94 | 95 | /// Checks the provided keyrequests 96 | /// and attempts to unseal the ed25519 keypair with `Aes128GcmSiv` 97 | pub fn unseal(sealed_data: &SealedKeyData) -> Result { 98 | if let Ok(public) = VerificationKey::try_from(&sealed_data.seal_key_request.keyid[..]) { 99 | let mut secret_key = unseal_secret(sealed_data)?; 100 | let mut secret = 101 | SigningKey::try_from(secret_key.as_ref()).map_err(|_| ErrorCode::InvalidSignature)?; 102 | secret_key.zeroize(); 103 | if secret 104 | .verification_key() 105 | .as_bytes() 106 | .ct_eq(public.as_bytes()) 107 | .unwrap_u8() 108 | == 0 109 | { 110 | secret.zeroize(); 111 | return Err(ErrorCode::InvalidSignature); 112 | } 113 | Ok(secret) 114 | } else { 115 | Err(ErrorCode::InvalidSignature) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /providers/sgx/sgx-app/src/sgx_app/state.rs: -------------------------------------------------------------------------------- 1 | use std::{io, net::TcpStream}; 2 | use tmkms_light::{ 3 | chain::state::{consensus, PersistStateSync, State, StateError}, 4 | utils::{read_u16_payload, write_u16_payload}, 5 | }; 6 | use tracing::debug; 7 | 8 | /// holds the connection for persiting the state outside of the enclave 9 | pub struct StateHolder { 10 | state_conn: TcpStream, 11 | } 12 | 13 | impl StateHolder { 14 | /// tries to connect to "state" address which is provided 15 | /// as "usercall extension" in the runner 16 | pub fn new() -> io::Result { 17 | Ok(Self { 18 | state_conn: TcpStream::connect("state")?, 19 | }) 20 | } 21 | } 22 | 23 | impl PersistStateSync for StateHolder { 24 | fn load_state(&mut self) -> Result { 25 | // TODO: currently unused as the initial state is now provided/loaded via "args" 26 | // so `PersistStateSync` is to be revisited 27 | let json_raw = read_u16_payload(&mut self.state_conn) 28 | .map_err(|e| StateError::sync_other_error(e.to_string()))?; 29 | let consensus_state: consensus::State = serde_json::from_slice(&json_raw) 30 | .map_err(|e| StateError::sync_enc_dec_error("error parsing state".into(), e))?; 31 | Ok(State::from(consensus_state)) 32 | } 33 | 34 | fn persist_state(&mut self, new_state: &consensus::State) -> Result<(), StateError> { 35 | debug!("writing new consensus state to state conn"); 36 | 37 | let json_raw = serde_json::to_vec(&new_state) 38 | .map_err(|e| StateError::sync_enc_dec_error("error serializing state".into(), e))?; 39 | 40 | write_u16_payload(&mut self.state_conn, &json_raw) 41 | .map_err(|e| StateError::sync_error("error state writing to socket".into(), e))?; 42 | 43 | debug!("successfully wrote new consensus state to state connection"); 44 | 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /providers/sgx/sgx-runner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmkms-light-sgx-runner" 3 | version = "0.4.2" 4 | authors = ["Tomas Tauber <2410580+tomtau@users.noreply.github.com>", "Linfeng Yuan "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | base64 = "0.21" 9 | serde = { version = "1", features = ["derive"] } 10 | ed25519 = { version = "2", features = ["serde"] } 11 | ed25519-consensus = { version = "2", features = ["serde"] } 12 | rsa = { version = "0.7", features = ["serde", "std"] } 13 | sgx-isa = { version = "0.4", features = ["serde"] } 14 | tendermint = "0.30" 15 | tendermint-config = "0.30" 16 | tmkms-light = { path = "../../.." } 17 | zeroize = "1" 18 | 19 | [target.'cfg(not(target_env = "sgx"))'.dependencies] 20 | aesm-client = { version = "0.5", features = ["sgxs"] } 21 | dcap-ql = "0.3" 22 | enclave-runner = "0.5" 23 | flex-error = "0.4" 24 | serde_json = "1" 25 | sgxs-loaders = "0.3" 26 | clap = {version = "4", features = ["derive"] } 27 | subtle-encoding = { version = "0.5", features = ["bech32-preview"] } 28 | tempfile = "3" 29 | tokio = { version = "= 0.2", features = ["uds"] } 30 | toml = "0.7" 31 | tracing = "0.1" 32 | tracing-subscriber = "0.3" 33 | -------------------------------------------------------------------------------- /providers/sgx/sgx-runner/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::CloudBackupKeyData; 2 | use crate::shared::SealedKeyData; 3 | use clap::Parser; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{convert::TryFrom, path::PathBuf}; 6 | use std::{fs::OpenOptions, io, os::unix::fs::OpenOptionsExt, path::Path}; 7 | use tendermint::chain; 8 | use tendermint_config::net; 9 | use tmkms_light::utils::PubkeyDisplay; 10 | use tracing::error; 11 | 12 | /// runner configuration in toml 13 | #[derive(Debug, Serialize, Deserialize)] 14 | #[serde(deny_unknown_fields)] 15 | pub struct SgxSignOpt { 16 | /// Address of the validator (`tcp://` or `unix://`) 17 | pub address: net::Address, 18 | /// Chain ID of the Tendermint network this validator is part of 19 | pub chain_id: chain::Id, 20 | /// Height at which to stop signing 21 | pub max_height: Option, 22 | /// Path to a file containing a cryptographic key 23 | pub sealed_consensus_key_path: PathBuf, 24 | /// Path to our Ed25519 identity key (if applicable) 25 | pub sealed_id_key_path: Option, 26 | /// Path to chain-specific `priv_validator_state.json` file 27 | pub state_file_path: PathBuf, 28 | /// Path to sgxs + signature files 29 | pub enclave_path: PathBuf, 30 | } 31 | 32 | impl Default for SgxSignOpt { 33 | fn default() -> Self { 34 | Self { 35 | address: net::Address::Unix { 36 | path: "/tmp/validator.socket".into(), 37 | }, 38 | chain_id: chain::Id::try_from("testchain-1".to_owned()).expect("valid chain-id"), 39 | max_height: None, 40 | sealed_consensus_key_path: "secrets/secret.key".into(), 41 | sealed_id_key_path: Some("secrets/id.key".into()), 42 | state_file_path: "state/priv_validator_state.json".into(), 43 | enclave_path: "enclave/tmkms-light-sgx-app.sgxs".into(), 44 | } 45 | } 46 | } 47 | 48 | fn write_json_file, T: ?Sized + Serialize>(path: P, data: &T) -> io::Result<()> { 49 | OpenOptions::new() 50 | .create(true) 51 | .write(true) 52 | .truncate(true) 53 | .mode(0o600) 54 | .open(path.as_ref()) 55 | .and_then(|file| { 56 | serde_json::to_writer(file, data).map_err(|e| { 57 | error!( 58 | "failed to write key (backup or sealed) data payload: {:?}", 59 | e 60 | ); 61 | io::ErrorKind::Other.into() 62 | }) 63 | }) 64 | } 65 | 66 | /// write sealed key data 67 | pub fn write_sealed_file>(path: P, sealed_data: &SealedKeyData) -> io::Result<()> { 68 | write_json_file(path, sealed_data) 69 | } 70 | 71 | /// write backup key data 72 | pub fn write_backup_file>( 73 | path: P, 74 | sealed_data: &CloudBackupKeyData, 75 | ) -> io::Result<()> { 76 | write_json_file(path, sealed_data) 77 | } 78 | 79 | #[derive(Parser, Debug)] 80 | pub struct RecoverConfig { 81 | #[arg(short)] 82 | pub config_path: Option, 83 | #[arg(short)] 84 | pub pubkey_display: Option, 85 | #[arg(short)] 86 | pub bech32_prefix: Option, 87 | #[arg(short)] 88 | pub wrap_backup_key_path: PathBuf, 89 | #[arg(short)] 90 | pub external_cloud_key_path: PathBuf, 91 | #[arg(short)] 92 | pub key_backup_data_path: PathBuf, 93 | #[arg(short)] 94 | pub recover_consensus_key: bool, 95 | } 96 | -------------------------------------------------------------------------------- /providers/sgx/sgx-runner/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod shared; 2 | 3 | pub use shared::*; 4 | -------------------------------------------------------------------------------- /providers/sgx/sgx-runner/src/main.rs: -------------------------------------------------------------------------------- 1 | mod command; 2 | mod config; 3 | mod runner; 4 | mod shared; 5 | mod state; 6 | use crate::config::RecoverConfig; 7 | use clap::Parser; 8 | use shared::SgxInitRequest; 9 | use std::fmt::Debug; 10 | use std::path::PathBuf; 11 | use tmkms_light::utils::PubkeyDisplay; 12 | use tracing::{error, Level}; 13 | use tracing_subscriber::FmtSubscriber; 14 | 15 | #[derive(Debug, Parser)] 16 | #[command( 17 | name = "tmkms-light-sgx-runner", 18 | about = "runner for signing backend app using SGX" 19 | )] 20 | enum TmkmsLight { 21 | #[command(name = "cloud-wrap", about = "Generate a wrap key for cloud backups")] 22 | /// Create config + keygen 23 | CloudWrapKeyGen { 24 | #[arg(short)] 25 | enclave_path: Option, 26 | #[arg(short)] 27 | sealed_wrap_key_path: Option, 28 | #[arg(short)] 29 | dcap: bool, 30 | /// log level, default: info, -v: info, -vv: debug, -vvv: trace 31 | #[arg(short, action = clap::ArgAction::Count)] 32 | v: u32, 33 | }, 34 | #[command(name = "init", about = "Create config and generate keys")] 35 | /// Create config + keygen 36 | Init { 37 | #[arg(short)] 38 | config_path: Option, 39 | #[arg(short)] 40 | pubkey_display: Option, 41 | #[arg(short)] 42 | bech32_prefix: Option, 43 | #[arg(short)] 44 | wrap_backup_key_path: Option, 45 | #[arg(short)] 46 | external_cloud_key_path: Option, 47 | #[arg(short)] 48 | key_backup_data_path: Option, 49 | #[arg(short, action = clap::ArgAction::Count)] 50 | v: u32, 51 | }, 52 | #[command(name = "recover", about = "Recover from cloud backup")] 53 | 54 | /// Recover from cloud backup payload 55 | Recover { 56 | #[command(flatten)] 57 | config: RecoverConfig, 58 | #[arg(short, action = clap::ArgAction::Count)] 59 | v: u32, 60 | }, 61 | #[command(name = "start", about = "Start tmkms process")] 62 | /// start tmkms process 63 | Start { 64 | #[arg(short)] 65 | config_path: Option, 66 | #[arg(short, action = clap::ArgAction::Count)] 67 | v: u32, 68 | }, 69 | } 70 | 71 | fn set_log(v: u32) -> String { 72 | let (log_level, log_level_str) = match v { 73 | 0 | 1 => (Level::INFO, "info"), 74 | 2 => (Level::DEBUG, "verbose"), 75 | _ => (Level::TRACE, "verbose"), 76 | }; 77 | let subscriber = FmtSubscriber::builder().with_max_level(log_level).finish(); 78 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 79 | log_level_str.into() 80 | } 81 | 82 | fn main() { 83 | let opt = TmkmsLight::parse(); 84 | let result = match opt { 85 | TmkmsLight::CloudWrapKeyGen { 86 | enclave_path, 87 | sealed_wrap_key_path, 88 | dcap, 89 | v, 90 | } => { 91 | let log_level_str = set_log(v); 92 | let enclave_path = 93 | enclave_path.unwrap_or_else(|| "enclave/tmkms-light-sgx-app.sgxs".into()); 94 | let sealed_wrap_key_path = 95 | sealed_wrap_key_path.unwrap_or_else(|| "sealed-wrap.key".into()); 96 | command::keywrap(enclave_path, sealed_wrap_key_path, dcap, log_level_str) 97 | } 98 | TmkmsLight::Init { 99 | config_path, 100 | pubkey_display, 101 | bech32_prefix, 102 | wrap_backup_key_path, 103 | external_cloud_key_path, 104 | key_backup_data_path, 105 | v, 106 | } => { 107 | let log_level_str = set_log(v); 108 | command::init( 109 | config_path, 110 | pubkey_display, 111 | bech32_prefix, 112 | wrap_backup_key_path, 113 | external_cloud_key_path, 114 | key_backup_data_path, 115 | log_level_str, 116 | ) 117 | } 118 | TmkmsLight::Start { config_path, v } => { 119 | let log_level_str = set_log(v); 120 | command::start(config_path, log_level_str) 121 | } 122 | TmkmsLight::Recover { config, v } => { 123 | let log_level_str = set_log(v); 124 | command::recover(config, log_level_str) 125 | } 126 | }; 127 | if let Err(e) = result { 128 | error!("{}", e); 129 | std::process::exit(1); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /providers/sgx/sgx-runner/src/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::{RemoteConnectionConfig, SealedKeyData, SgxInitRequest, SgxInitResponse}; 2 | use crate::state::StateSyncer; 3 | use aesm_client::AesmClient; 4 | use enclave_runner::{ 5 | usercalls::{AsyncStream, UsercallExtension}, 6 | EnclaveBuilder, 7 | }; 8 | use sgxs_loaders::isgx::Device; 9 | use std::os::unix::net::UnixStream; 10 | use std::path::Path; 11 | use std::thread; 12 | use std::{fs, path::PathBuf}; 13 | use std::{future::Future, io, pin::Pin}; 14 | use tendermint::consensus; 15 | use tendermint_config::net; 16 | use tmkms_light::config::validator::ValidatorConfig; 17 | use tmkms_light::error::{io_error_wrap, Error}; 18 | use tmkms_light::utils::read_u16_payload; 19 | use tracing::{debug, error}; 20 | 21 | /// type alias for outputs in UsercallExtension async return type 22 | type UserCallStream = io::Result>>; 23 | 24 | /// custom runner for tmkms <-> enclave app communication 25 | /// TODO: Windows support (via random TCP or custom in-memory stream)? 26 | #[derive(Debug)] 27 | struct TmkmsSgxRunner { 28 | init_stream: UnixStream, 29 | state_stream: UnixStream, 30 | tm_conn: Option, 31 | } 32 | 33 | impl UsercallExtension for TmkmsSgxRunner { 34 | fn connect_stream<'future>( 35 | &'future self, 36 | addr: &'future str, 37 | _local_addr: Option<&'future mut String>, 38 | _peer_addr: Option<&'future mut String>, 39 | ) -> Pin + 'future>> { 40 | async fn connect_stream_inner(this: &TmkmsSgxRunner, addr: &str) -> UserCallStream { 41 | match addr { 42 | "init" => { 43 | let stream = tokio::net::UnixStream::from_std(this.init_stream.try_clone()?)?; 44 | Ok(Some(Box::new(stream))) 45 | } 46 | "state" => { 47 | let stream = tokio::net::UnixStream::from_std(this.state_stream.try_clone()?)?; 48 | Ok(Some(Box::new(stream))) 49 | } 50 | "tendermint" => { 51 | if let Some(ref path) = this.tm_conn { 52 | let stream = tokio::net::UnixStream::connect(path).await?; 53 | Ok(Some(Box::new(stream))) 54 | } else { 55 | Ok(None) 56 | } 57 | } 58 | _ => Ok(None), 59 | } 60 | } 61 | Box::pin(connect_stream_inner(self, addr)) 62 | } 63 | } 64 | 65 | /// controller for launching the enclave app and providing the communication with it 66 | pub struct TmkmsSgxSigner { 67 | stream_to_enclave: UnixStream, 68 | enclave_app_thread: thread::JoinHandle>, 69 | } 70 | 71 | impl TmkmsSgxSigner { 72 | /// returns the state persistence helper, the last persisted state, 73 | /// and the unix socket to pass to the enclave runner 74 | pub fn get_state_syncer>( 75 | state_path: P, 76 | ) -> Result<(StateSyncer, consensus::State, UnixStream), Error> { 77 | let (state_from_enclave, state_stream) = UnixStream::pair() 78 | .map_err(|e| Error::io_error("failed to get state unix socket pair".into(), e))?; 79 | 80 | let (state_syncer, state) = StateSyncer::new(state_path, state_from_enclave) 81 | .map_err(|e| io_error_wrap("failed to get state persistence helper".into(), e))?; 82 | Ok((state_syncer, state, state_stream)) 83 | } 84 | 85 | /// launches the `tmkms-light-sgx-app` from the provided path 86 | pub fn launch_enclave_app>( 87 | sgxs_path: P, 88 | tm_conn: Option, 89 | state_syncer: StateSyncer, 90 | state_stream: UnixStream, 91 | args: &[&[u8]], 92 | ) -> io::Result { 93 | let (stream_to_enclave, init_stream) = UnixStream::pair()?; 94 | state_syncer.launch_syncer(); 95 | let runner = TmkmsSgxRunner { 96 | init_stream, 97 | state_stream, 98 | tm_conn, 99 | }; 100 | let mut device = Device::new()? 101 | .einittoken_provider(AesmClient::new()) 102 | .build(); 103 | let mut enclave_builder = EnclaveBuilder::new(sgxs_path.as_ref()); 104 | enclave_builder.coresident_signature()?; 105 | 106 | enclave_builder.usercall_extension(runner); 107 | enclave_builder.forward_panics(true); 108 | enclave_builder.args(args); 109 | match enclave_builder.build(&mut device) { 110 | Ok(enclave) => { 111 | let enclave_app_thread = thread::spawn(|| { 112 | enclave 113 | .run() 114 | .map_err(|e| io_error_wrap("enclave runner error".into(), e)) 115 | }); 116 | Ok(Self { 117 | stream_to_enclave, 118 | enclave_app_thread, 119 | }) 120 | } 121 | Err(e) => { 122 | error!("failed to build the enclave app: {:?}", e); 123 | Err(io::ErrorKind::Other.into()) 124 | } 125 | } 126 | } 127 | 128 | /// get the response from the enclave via the init stream 129 | pub fn get_init_response(mut self) -> Result { 130 | debug!("waiting for response"); 131 | let response_bytes = read_u16_payload(&mut self.stream_to_enclave)?; 132 | let resp: SgxInitResponse = serde_json::from_slice(&response_bytes) 133 | .map_err(|e| io_error_wrap("error deserializing response".into(), e))?; 134 | self.join_enclave_thread()?; 135 | Ok(resp) 136 | } 137 | 138 | /// get the request payload that's passed as an argument to the enclave 139 | /// to start up the tmkms handling 140 | pub fn get_start_request_bytes>( 141 | sealed_key_path: P, 142 | config: ValidatorConfig, 143 | initial_state: consensus::State, 144 | remote_conn: Option<(net::Address, P)>, 145 | ) -> Result, Error> { 146 | let sealed_key: SealedKeyData = serde_json::from_slice( 147 | &fs::read(sealed_key_path) 148 | .map_err(|e| Error::io_error("error reading sealed consensus key".into(), e))?, 149 | ) 150 | .map_err(|_e| Error::invalid_key_error())?; 151 | let secret_connection = match remote_conn { 152 | Some(( 153 | net::Address::Tcp { 154 | peer_id, 155 | host, 156 | port, 157 | }, 158 | id_path, 159 | )) => { 160 | let sealed_id_key: SealedKeyData = serde_json::from_slice( 161 | &fs::read(id_path) 162 | .map_err(|e| Error::io_error("error reading id sealed key".into(), e))?, 163 | ) 164 | .map_err(|_e| Error::invalid_key_error())?; 165 | Some(RemoteConnectionConfig { 166 | peer_id, 167 | host, 168 | port, 169 | sealed_key: sealed_id_key, 170 | }) 171 | } 172 | _ => None, 173 | }; 174 | let req_bytes = serde_json::to_vec(&SgxInitRequest::Start { 175 | sealed_key, 176 | config, 177 | secret_connection, 178 | initial_state, 179 | }) 180 | .map_err(|e| io_error_wrap("failed to obtain the start request payload".into(), e))?; 181 | Ok(req_bytes) 182 | } 183 | 184 | fn join_enclave_thread(self) -> Result<(), Error> { 185 | match self.enclave_app_thread.join() { 186 | Ok(Ok(_)) => Ok(()), 187 | Err(_e) => Err(Error::panic_error()), 188 | Ok(Err(e)) => Err(e), 189 | } 190 | } 191 | 192 | /// run the main privval handling 193 | pub fn start(self) -> Result<(), Error> { 194 | self.join_enclave_thread() 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /providers/sgx/sgx-runner/src/shared.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose, Engine as _}; 2 | use rsa::PublicKeyParts; 3 | use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; 4 | use sgx_isa::{Keypolicy, Keyrequest, Report, Targetinfo}; 5 | use std::convert::TryInto; 6 | use std::fmt; 7 | use std::str::FromStr; 8 | use tendermint::consensus; 9 | use tendermint::node; 10 | use tmkms_light::config::validator::ValidatorConfig; 11 | 12 | /// keyseal is fixed in the enclave app 13 | pub type AesGcmSivNonce = [u8; 12]; 14 | 15 | /// from sealing the ed25519 keypairs + RSA PKCS1 16 | pub type Ciphertext = Vec; 17 | 18 | /// this partially duplicates `Keyrequest` from sgx-isa, 19 | /// which doesn't implement serde traits 20 | #[derive(Debug, Serialize, Deserialize, Copy, Clone)] 21 | pub struct KeyRequestWrap { 22 | pub keyname: u16, 23 | pub keypolicy: u16, 24 | pub isvsvn: u16, 25 | pub cpusvn: [u8; 16], 26 | pub attributemask: [u64; 2], 27 | pub keyid: PublicKey, 28 | pub miscmask: u32, 29 | } 30 | 31 | impl TryInto for KeyRequestWrap { 32 | type Error = (); 33 | 34 | fn try_into(self) -> Result { 35 | let keypolicy = Keypolicy::from_bits(self.keypolicy).ok_or(())?; 36 | Ok(Keyrequest { 37 | keyname: self.keyname, 38 | keypolicy, 39 | isvsvn: self.isvsvn, 40 | cpusvn: self.cpusvn, 41 | attributemask: self.attributemask, 42 | keyid: self.keyid, 43 | miscmask: self.miscmask, 44 | ..Default::default() 45 | }) 46 | } 47 | } 48 | 49 | impl From for KeyRequestWrap { 50 | fn from(kr: Keyrequest) -> Self { 51 | KeyRequestWrap { 52 | keyname: kr.keyname, 53 | keypolicy: kr.keypolicy.bits(), 54 | isvsvn: kr.isvsvn, 55 | cpusvn: kr.cpusvn, 56 | attributemask: kr.attributemask, 57 | keyid: kr.keyid, 58 | miscmask: kr.miscmask, 59 | } 60 | } 61 | } 62 | 63 | /// Returned from the enclave app after keygen 64 | /// and expected to be persisted by tmkms 65 | #[derive(Debug, Serialize, Deserialize, Clone)] 66 | pub struct SealedKeyData { 67 | pub seal_key_request: KeyRequestWrap, 68 | pub nonce: AesGcmSivNonce, 69 | pub sealed_secret: Ciphertext, 70 | } 71 | 72 | /// ed25519 pubkey alias 73 | pub type PublicKey = [u8; 32]; 74 | 75 | /// Returned from the enclave app after keygen 76 | /// if the cloud backup option is requested. 77 | /// This may be needed in cloud settings 78 | /// without HW/CPU affinity, as instances 79 | /// may be relocated and fail to unseal `SealedKeyData` 80 | #[derive(Debug, Serialize, Deserialize, Clone)] 81 | pub struct CloudBackupKeyData { 82 | pub nonce: AesGcmSivNonce, 83 | pub sealed_secret: Ciphertext, 84 | pub public_key: ed25519_consensus::VerificationKey, 85 | } 86 | 87 | /// configuration for direct remote communication with TM 88 | #[derive(Debug, Serialize, Deserialize)] 89 | pub struct RemoteConnectionConfig { 90 | /// Remote peer ID 91 | pub peer_id: Option, 92 | 93 | /// Hostname or IP address 94 | pub host: String, 95 | 96 | /// Port 97 | pub port: u16, 98 | 99 | /// Sealed key for TM SecretConnection 100 | pub sealed_key: SealedKeyData, 101 | } 102 | 103 | /// package for cloud backups 104 | #[derive(Debug, Serialize, Deserialize, Clone)] 105 | pub struct CloudBackupKey { 106 | pub sealed_rsa_key: SealedKeyData, 107 | pub backup_key: CloudBackupSeal, 108 | } 109 | 110 | /// payload from cloud environments for backups 111 | #[derive(Debug, Clone)] 112 | pub struct CloudBackupSeal { 113 | /// kek encrypted using RSA-OAEP+SHA-256 114 | pub encrypted_symmetric_key: [u8; 256], 115 | /// 32-byte key that can be unwrapped with the decrypted kek 116 | /// using RFC3394/RFC5649 AES-KWP 117 | pub wrapped_cloud_sealing_key: [u8; 40], 118 | } 119 | 120 | impl CloudBackupSeal { 121 | /// returns the struct if the payload length matches the expected on 122 | pub fn new(payload: Vec) -> Option { 123 | if payload.len() != 296 { 124 | None 125 | } else { 126 | let mut encrypted_symmetric_key = [0u8; 256]; 127 | encrypted_symmetric_key.copy_from_slice(&payload[..256]); 128 | let mut wrapped_cloud_sealing_key = [0u8; 40]; 129 | wrapped_cloud_sealing_key.copy_from_slice(&payload[256..]); 130 | Some(Self { 131 | encrypted_symmetric_key, 132 | wrapped_cloud_sealing_key, 133 | }) 134 | } 135 | } 136 | } 137 | 138 | impl Serialize for CloudBackupSeal { 139 | fn serialize(&self, serializer: S) -> std::result::Result 140 | where 141 | S: Serializer, 142 | { 143 | serializer.serialize_str(&self.to_string()) 144 | } 145 | } 146 | 147 | impl<'de> Deserialize<'de> for CloudBackupSeal { 148 | fn deserialize(deserializer: D) -> Result 149 | where 150 | D: Deserializer<'de>, 151 | { 152 | struct StrVisitor; 153 | 154 | impl<'de> de::Visitor<'de> for StrVisitor { 155 | type Value = CloudBackupSeal; 156 | 157 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 158 | formatter.write_str("CloudBackupSeal") 159 | } 160 | 161 | #[inline] 162 | fn visit_str(self, value: &str) -> Result 163 | where 164 | E: de::Error, 165 | { 166 | CloudBackupSeal::from_str(value).map_err(|err| de::Error::custom(err.to_string())) 167 | } 168 | } 169 | 170 | deserializer.deserialize_str(StrVisitor) 171 | } 172 | } 173 | 174 | impl fmt::Display for CloudBackupSeal { 175 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 176 | let full = [ 177 | &self.encrypted_symmetric_key[..], 178 | &self.wrapped_cloud_sealing_key[..], 179 | ] 180 | .concat(); 181 | let full_str = general_purpose::STANDARD.encode(full); 182 | write!(f, "{}", full_str) 183 | } 184 | } 185 | 186 | impl FromStr for CloudBackupSeal { 187 | type Err = base64::DecodeError; 188 | 189 | fn from_str(s: &str) -> Result { 190 | let bytes = general_purpose::STANDARD.decode(s)?; 191 | CloudBackupSeal::new(bytes).ok_or(base64::DecodeError::InvalidLength) 192 | } 193 | } 194 | 195 | /// request sent to the enclave app 196 | /// TODO: check the clippy size diff warning (it may not be worth it, given these are one-off requests) 197 | #[derive(Debug, Serialize, Deserialize)] 198 | #[allow(clippy::large_enum_variant)] 199 | pub enum SgxInitRequest { 200 | /// generates a wrapping key for cloud backups 201 | GenWrapKey { 202 | /// if dcap is used 203 | targetinfo: Option, 204 | }, 205 | /// generate a new keypair 206 | KeyGen { 207 | cloud_backup: Option, 208 | }, 209 | /// reseal the keypair from a backup 210 | CloudRecover { 211 | cloud_backup: CloudBackupKey, 212 | key_data: CloudBackupKeyData, 213 | }, 214 | /// start the main loop for processing Tendermint privval requests 215 | Start { 216 | sealed_key: SealedKeyData, 217 | config: ValidatorConfig, 218 | secret_connection: Option, 219 | initial_state: consensus::State, 220 | }, 221 | } 222 | 223 | /// response sent from the enclave app 224 | /// TODO: check the clippy size diff warning (it may not be worth it, given these are one-off requests) 225 | #[derive(Debug, Serialize, Deserialize)] 226 | #[allow(clippy::large_enum_variant)] 227 | pub enum SgxInitResponse { 228 | WrapKey { 229 | /// key sealed for local CPU 230 | wrap_key_sealed: SealedKeyData, 231 | /// wrapping public key 232 | wrap_pub_key: rsa::RsaPublicKey, 233 | /// report attesting the wrapping public key 234 | /// (to be used for a quote) 235 | pub_key_report: Report, 236 | }, 237 | /// response to key generation or recovery 238 | GenOrRecover { 239 | /// freshly generated or recovered sealed keypair 240 | sealed_key_data: SealedKeyData, 241 | /// if requested, keypair encrypted with the provided key 242 | cloud_backup_key_data: Option, 243 | }, 244 | } 245 | 246 | /// obtain a json claim for RSA pubkey 247 | /// (bigendian values are encoded in URL-safe base64) 248 | pub fn get_claim(wrap_pub_key: &rsa::RsaPublicKey) -> String { 249 | let n = wrap_pub_key.n().to_bytes_be(); 250 | let e = wrap_pub_key.e().to_bytes_be(); 251 | let encoded_n = general_purpose::URL_SAFE.encode(n); 252 | let encoded_e = general_purpose::URL_SAFE.encode(e); 253 | format!( 254 | "{{\"kid\":\"wrapping-key\",\"kty\":\"RSA\",\"e\":\"{}\",\"n\":\"{}\"}}", 255 | encoded_e, encoded_n 256 | ) 257 | } 258 | 259 | impl SgxInitResponse { 260 | /// get key generation or recovery response 261 | pub fn get_gen_response(self) -> Option<(SealedKeyData, Option)> { 262 | match self { 263 | SgxInitResponse::GenOrRecover { 264 | sealed_key_data, 265 | cloud_backup_key_data, 266 | } => Some((sealed_key_data, cloud_backup_key_data)), 267 | _ => None, 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /providers/sgx/sgx-runner/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::net::UnixStream; 2 | use std::thread; 3 | use std::{ 4 | fs, 5 | io::{self, prelude::*}, 6 | path::{Path, PathBuf}, 7 | }; 8 | use tempfile::NamedTempFile; 9 | use tmkms_light::chain::state::{consensus, StateError}; 10 | use tmkms_light::utils::read_u16_payload; 11 | use tracing::{debug, warn}; 12 | 13 | pub struct StateSyncer { 14 | state_file_path: PathBuf, 15 | stream_to_enclave: UnixStream, 16 | } 17 | 18 | impl StateSyncer { 19 | pub fn new>( 20 | path: P, 21 | stream_to_enclave: UnixStream, 22 | ) -> Result<(Self, consensus::State), StateError> { 23 | let state_file_path = path.as_ref().to_owned(); 24 | let state = match fs::read_to_string(&path) { 25 | Ok(state_json) => { 26 | let consensus_state: consensus::State = serde_json::from_str(&state_json) 27 | .map_err(|e| StateError::sync_enc_dec_error("error parsing".into(), e))?; 28 | 29 | Ok(consensus_state) 30 | } 31 | Err(e) if e.kind() == io::ErrorKind::NotFound => { 32 | Self::write_initial_state(&state_file_path) 33 | } 34 | Err(e) => Err(StateError::sync_error( 35 | path.as_ref().display().to_string(), 36 | e, 37 | )), 38 | }?; 39 | Ok(( 40 | Self { 41 | state_file_path, 42 | stream_to_enclave, 43 | }, 44 | state, 45 | )) 46 | } 47 | 48 | /// load state from the provided vsock stream 49 | fn sync_from_stream(&mut self) -> Result { 50 | let json_raw = read_u16_payload(&mut self.stream_to_enclave) 51 | .map_err(|e| StateError::sync_other_error(e.to_string()))?; 52 | 53 | serde_json::from_slice(&json_raw) 54 | .map_err(|e| StateError::sync_enc_dec_error("failed to deserialize state".into(), e)) 55 | } 56 | 57 | /// Write the initial state to the given path on disk 58 | fn write_initial_state(path: &Path) -> Result { 59 | let consensus_state = consensus::State { 60 | height: 0u32.into(), 61 | ..Default::default() 62 | }; 63 | 64 | Self::persist_state(path, &consensus_state)?; 65 | 66 | Ok(consensus_state) 67 | } 68 | 69 | /// Launches the state syncer 70 | pub fn launch_syncer(mut self) { 71 | thread::spawn(move || loop { 72 | if let Ok(ref consensus_state) = self.sync_from_stream() { 73 | if let Err(e) = Self::persist_state(&self.state_file_path, consensus_state) { 74 | warn!("state persistence failed: {}", e); 75 | } 76 | } 77 | }); 78 | } 79 | 80 | fn persist_state(path: &Path, new_state: &consensus::State) -> Result<(), StateError> { 81 | debug!( 82 | "writing new consensus state to {}: {:?}", 83 | path.display(), 84 | &new_state 85 | ); 86 | 87 | let json = serde_json::to_string(&new_state) 88 | .map_err(|e| StateError::sync_enc_dec_error(path.display().to_string(), e))?; 89 | 90 | let state_file_dir = path.parent().unwrap_or_else(|| { 91 | panic!("state file cannot be root directory"); 92 | }); 93 | 94 | let mut state_file = NamedTempFile::new_in(state_file_dir) 95 | .map_err(|e| StateError::sync_error(path.display().to_string(), e))?; 96 | 97 | state_file 98 | .write_all(json.as_bytes()) 99 | .map_err(|e| StateError::sync_error(path.display().to_string(), e))?; 100 | 101 | state_file 102 | .persist(path) 103 | .map_err(|e| StateError::sync_error(path.display().to_string(), e.error))?; 104 | 105 | debug!( 106 | "successfully wrote new consensus state to {}", 107 | path.display(), 108 | ); 109 | 110 | Ok(()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /providers/softsign/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmkms-softsign" 3 | version = "0.4.2" 4 | authors = ["Tomas Tauber <2410580+tomtau@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | flex-error = "0.4" 11 | ed25519-consensus = "2" 12 | rand_core = { version = "0.6", features = ["std"] } 13 | serde = { version = "1", features = ["serde_derive"] } 14 | serde_json = "1" 15 | clap = {version = "4", features = ["derive"] } 16 | subtle = "2" 17 | subtle-encoding = { version = "0.5", features = ["bech32-preview"] } 18 | tempfile = "3" 19 | tendermint = "0.30" 20 | tendermint-config = "0.30" 21 | tendermint-p2p = "0.30" 22 | tmkms-light = { path = "../.." } 23 | tracing = "0.1" 24 | tracing-subscriber = "0.3" 25 | toml = "0.7" 26 | zeroize = "1" 27 | -------------------------------------------------------------------------------- /providers/softsign/src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{convert::TryFrom, path::PathBuf}; 3 | use tendermint::chain; 4 | use tendermint_config::net; 5 | 6 | #[derive(Debug, Serialize, Deserialize)] 7 | #[serde(deny_unknown_fields)] 8 | pub struct SoftSignOpt { 9 | /// Address of the validator (`tcp://` or `unix://`) 10 | pub address: net::Address, 11 | /// Chain ID of the Tendermint network this validator is part of 12 | pub chain_id: chain::Id, 13 | /// Height at which to stop signing 14 | pub max_height: Option, 15 | /// Path to a file containing a cryptographic key 16 | pub consensus_key_path: PathBuf, 17 | /// Path to our Ed25519 identity key (if applicable) 18 | pub id_key_path: Option, 19 | /// Path to chain-specific `priv_validator_state.json` file 20 | pub state_file_path: PathBuf, 21 | /// Optional timeout value in seconds 22 | pub timeout: Option, 23 | /// Retry connection 24 | pub retry: bool, 25 | } 26 | 27 | impl Default for SoftSignOpt { 28 | fn default() -> Self { 29 | Self { 30 | address: net::Address::Unix { 31 | path: "/tmp/validator.socket".into(), 32 | }, 33 | chain_id: chain::Id::try_from("testchain-1".to_owned()).expect("valid chain-id"), 34 | max_height: None, 35 | consensus_key_path: "secrets/secret.key".into(), 36 | id_key_path: Some("secrets/id.key".into()), 37 | state_file_path: "state/priv_validator_state.json".into(), 38 | timeout: None, 39 | retry: true, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /providers/softsign/src/key_utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities 2 | 3 | use std::{ 4 | fs::{self, OpenOptions}, 5 | io::Write, 6 | os::unix::fs::OpenOptionsExt, 7 | path::Path, 8 | }; 9 | 10 | use ed25519::SigningKey; 11 | use ed25519_consensus as ed25519; 12 | use rand_core::OsRng; 13 | use subtle_encoding::base64; 14 | use tmkms_light::error::{io_error_wrap, Error}; 15 | use zeroize::Zeroizing; 16 | 17 | /// File permissions for secret data 18 | pub const SECRET_FILE_PERMS: u32 = 0o600; 19 | 20 | /// Load Base64-encoded secret data (i.e. key) from the given path 21 | pub fn load_base64_secret(path: impl AsRef) -> Result>, Error> { 22 | // TODO(tarcieri): check file permissions are correct 23 | let base64_data = Zeroizing::new(fs::read_to_string(path.as_ref()).map_err(|e| { 24 | Error::io_error( 25 | format!("couldn't read key from {}: {}", path.as_ref().display(), e), 26 | e, 27 | ) 28 | })?); 29 | 30 | // TODO(tarcieri): constant-time string trimming 31 | let data = Zeroizing::new(base64::decode(base64_data.trim_end()).map_err(|e| { 32 | io_error_wrap( 33 | format!("can't decode key from `{}`: {}", path.as_ref().display(), e), 34 | e, 35 | ) 36 | })?); 37 | 38 | Ok(data) 39 | } 40 | 41 | /// Load a Base64-encoded Ed25519 secret key 42 | pub fn load_base64_ed25519_key(path: impl AsRef) -> Result { 43 | let key_bytes = load_base64_secret(path)?; 44 | 45 | let secret = 46 | ed25519::SigningKey::try_from(&key_bytes[..]).map_err(|_e| Error::invalid_key_error())?; 47 | 48 | Ok(secret) 49 | } 50 | 51 | /// Store Base64-encoded secret data at the given path 52 | pub fn write_base64_secret(path: impl AsRef, data: &[u8]) -> Result<(), Error> { 53 | let base64_data = Zeroizing::new(base64::encode(data)); 54 | 55 | OpenOptions::new() 56 | .create(true) 57 | .write(true) 58 | .truncate(true) 59 | .mode(SECRET_FILE_PERMS) 60 | .open(path.as_ref()) 61 | .and_then(|mut file| file.write_all(&base64_data)) 62 | .map_err(|e| { 63 | Error::io_error( 64 | format!("couldn't write `{}`: {}", path.as_ref().display(), e), 65 | e, 66 | ) 67 | }) 68 | } 69 | 70 | /// Generate a Secret Connection key at the given path 71 | #[allow(clippy::explicit_auto_deref)] 72 | pub fn generate_key(path: impl AsRef) -> Result<(), Error> { 73 | let secret_key = SigningKey::new(OsRng); 74 | write_base64_secret(path, &secret_key.as_bytes()[..]) 75 | } 76 | -------------------------------------------------------------------------------- /providers/softsign/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | io::{self, prelude::*}, 4 | path::{Path, PathBuf}, 5 | }; 6 | use tempfile::NamedTempFile; 7 | use tmkms_light::chain::state::{consensus, PersistStateSync, State, StateError}; 8 | use tracing::debug; 9 | 10 | pub struct StateHolder { 11 | state_file_path: PathBuf, 12 | } 13 | 14 | impl StateHolder { 15 | pub fn new>(path: P) -> Self { 16 | Self { 17 | state_file_path: path.as_ref().to_owned(), 18 | } 19 | } 20 | 21 | /// Write the initial state to the given path on disk 22 | fn write_initial_state(&mut self) -> Result { 23 | let consensus_state = consensus::State { 24 | height: 0u32.into(), 25 | ..Default::default() 26 | }; 27 | 28 | self.persist_state(&consensus_state)?; 29 | 30 | Ok(State::from(consensus_state)) 31 | } 32 | } 33 | 34 | impl PersistStateSync for StateHolder { 35 | fn load_state(&mut self) -> Result { 36 | match fs::read_to_string(&self.state_file_path) { 37 | Ok(state_json) => { 38 | let consensus_state: consensus::State = 39 | serde_json::from_str(&state_json).map_err(|e| { 40 | StateError::sync_enc_dec_error( 41 | self.state_file_path.display().to_string(), 42 | e, 43 | ) 44 | })?; 45 | 46 | Ok(State::from(consensus_state)) 47 | } 48 | Err(e) if e.kind() == io::ErrorKind::NotFound => self.write_initial_state(), 49 | Err(e) => Err(StateError::sync_error( 50 | self.state_file_path.display().to_string(), 51 | e, 52 | )), 53 | } 54 | } 55 | 56 | fn persist_state(&mut self, new_state: &consensus::State) -> Result<(), StateError> { 57 | debug!( 58 | "writing new consensus state to {}: {:?}", 59 | self.state_file_path.display(), 60 | &new_state 61 | ); 62 | 63 | let json = serde_json::to_string(&new_state).map_err(|e| { 64 | StateError::sync_enc_dec_error(self.state_file_path.display().to_string(), e) 65 | })?; 66 | 67 | let state_file_dir = self.state_file_path.parent().unwrap_or_else(|| { 68 | panic!("state file cannot be root directory"); 69 | }); 70 | 71 | let mut state_file = NamedTempFile::new_in(state_file_dir) 72 | .map_err(|e| StateError::sync_error(self.state_file_path.display().to_string(), e))?; 73 | state_file 74 | .write_all(json.as_bytes()) 75 | .map_err(|e| StateError::sync_error(self.state_file_path.display().to_string(), e))?; 76 | state_file.persist(&self.state_file_path).map_err(|e| { 77 | StateError::sync_error(self.state_file_path.display().to_string(), e.error) 78 | })?; 79 | 80 | debug!( 81 | "successfully wrote new consensus state to {}", 82 | self.state_file_path.display(), 83 | ); 84 | 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /script/tmkms-nitro/default.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | ( let 4 | cose = pkgs.python39Packages.buildPythonPackage rec { 5 | pname = "cose"; 6 | version = "0.9.dev7"; 7 | 8 | src = pkgs.python39Packages.fetchPypi{ 9 | inherit pname version; 10 | sha256 = "d82cb1ebcdc5c759c1307f7302c5e6cb327d4195c03c31cb5fbdf6851a74d7ea"; 11 | }; 12 | doCheck = false; 13 | preConfigure = '' 14 | touch requirements.txt 15 | ''; 16 | }; 17 | attr = pkgs.python39Packages.buildPythonPackage rec { 18 | pname = "attrs"; 19 | version = "21.2.0"; 20 | 21 | src = pkgs.python39Packages.fetchPypi{ 22 | inherit pname version; 23 | sha256 = "ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"; 24 | }; 25 | doCheck = false; 26 | 27 | }; 28 | 29 | in pkgs.python39.buildEnv.override rec { 30 | 31 | extraLibs = [ pkgs.python39Packages.pycryptodome pkgs.python39Packages.cbor2 cose attr pkgs.python39Packages.cryptography pkgs.python39Packages.ecdsa pkgs.python39Packages.pyopenssl ]; 32 | } 33 | ).env 34 | -------------------------------------------------------------------------------- /script/tmkms-nitro/verify.py: -------------------------------------------------------------------------------- 1 | # bech32 Copyright (c) 2017, Pieter Wuille (licensed under the MIT License) 2 | # attestation verifier Copyright (c) 2020, Richard Fan (licensed under the Apache License, Version 2.0) 3 | # Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 4 | import base64 5 | import json 6 | import sys 7 | from datetime import datetime 8 | 9 | import cbor2 10 | from cose.keys.curves import P384 11 | from cose.keys.ec2 import EC2 12 | from cose.messages import Sign1Message 13 | from Crypto.Util.number import long_to_bytes 14 | from OpenSSL import crypto 15 | 16 | CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 17 | 18 | def bech32_polymod(values): 19 | """Internal function that computes the Bech32 checksum.""" 20 | generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] 21 | chk = 1 22 | for value in values: 23 | top = chk >> 25 24 | chk = (chk & 0x1ffffff) << 5 ^ value 25 | for i in range(5): 26 | chk ^= generator[i] if ((top >> i) & 1) else 0 27 | return chk 28 | 29 | 30 | def bech32_hrp_expand(hrp): 31 | """Expand the HRP into values for checksum computation.""" 32 | return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] 33 | 34 | 35 | def bech32_verify_checksum(hrp, data): 36 | """Verify a checksum given HRP and converted data characters.""" 37 | return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 38 | 39 | 40 | def bech32_create_checksum(hrp, data): 41 | """Compute the checksum values given HRP and data.""" 42 | values = bech32_hrp_expand(hrp) + data 43 | polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 44 | return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] 45 | 46 | 47 | def convertbits(data, frombits, tobits, pad=True): 48 | """General power-of-2 base conversion.""" 49 | acc = 0 50 | bits = 0 51 | ret = [] 52 | maxv = (1 << tobits) - 1 53 | max_acc = (1 << (frombits + tobits - 1)) - 1 54 | for value in data: 55 | if value < 0 or (value >> frombits): 56 | return None 57 | acc = ((acc << frombits) | value) & max_acc 58 | bits += frombits 59 | while bits >= tobits: 60 | bits -= tobits 61 | ret.append((acc >> bits) & maxv) 62 | if pad: 63 | if bits: 64 | ret.append((acc << (tobits - bits)) & maxv) 65 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 66 | return None 67 | return ret 68 | 69 | def bech32_encode(hrp, data): 70 | """Compute a Bech32 string given HRP and data values.""" 71 | # with amino prefix 72 | data = [0x16, 0x24, 0xDE, 0x64, 0x20] + data 73 | bdata = convertbits(data, 8, 5) 74 | combined = bdata + bech32_create_checksum(hrp, bdata) 75 | return hrp + '1' + ''.join([CHARSET[d] for d in combined]) 76 | 77 | def verify_attestation_doc(attestation_doc, root_cert_pem = None, bech32hrp = "crocnclconspub"): 78 | """ 79 | Verify the attestation document 80 | If invalid, raise an exception 81 | """ 82 | # Decode CBOR attestation document 83 | data = cbor2.loads(attestation_doc) 84 | 85 | # Load and decode document payload 86 | doc = data[2] 87 | doc_obj = cbor2.loads(doc) 88 | 89 | # https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md 90 | # 3.2 91 | if 'module_id' not in doc_obj or len(doc_obj["module_id"]) == 0: 92 | raise Exception("module_id error") 93 | if 'digest' not in doc_obj or doc_obj["digest"] != "SHA384": 94 | raise Exception("digest error") 95 | if 'timestamp' not in doc_obj or doc_obj["timestamp"] == 0: 96 | raise Exception("timestamp error") 97 | time = datetime.fromtimestamp(doc_obj["timestamp"] / 1000) 98 | print(f"timestamp: {time}") 99 | # PCRs are printed for verification below 100 | if 'pcrs' not in doc_obj or len(doc_obj["pcrs"]) > 32 or len(doc_obj["pcrs"]) == 0: 101 | raise Exception("pcrs error") 102 | if 'certificate' not in doc_obj: 103 | raise Exception("certificate error") 104 | if 'cabundle' not in doc_obj or len(doc_obj["cabundle"]) == 0: 105 | raise Exception("cabundle error") 106 | # we also expect user data 107 | if 'user_data' not in doc_obj: 108 | raise Exception("user_data error") 109 | user_data = json.loads(doc_obj['user_data']) 110 | if 'pubkey' not in user_data or 'key_id' not in user_data: 111 | raise Exception("user_data error") 112 | key_id = base64.b64decode(user_data["key_id"]) 113 | pubkey = base64.b64decode(user_data["pubkey"]) 114 | pubkeyb64 = user_data["pubkey"] 115 | pubkeyb32 = bech32_encode(bech32hrp, list(pubkey)) 116 | print("*** VERIFY user_data below (used AWS KMS key and generated pubkey) ***") 117 | print(f"AWS KMS key id: {key_id}") 118 | print(f"validator pubkey (base64): {pubkeyb64}") 119 | print(f"validator pubkey (bech32): {pubkeyb32}") 120 | 121 | # Get PCRs from attestation document 122 | document_pcrs_arr = doc_obj['pcrs'] 123 | 124 | ########################### 125 | # Part 1: Validating PCRs # 126 | ########################### 127 | print("*** VERIFY PCRs below are complete and correct ***") 128 | for index in document_pcrs_arr.keys(): 129 | 130 | # Get PCR hexcode 131 | doc_pcr = document_pcrs_arr[index].hex() 132 | 133 | print(f"PCR{index}: {doc_pcr}") 134 | 135 | 136 | ################################ 137 | # Part 2: Validating signature # 138 | ################################ 139 | 140 | # Get signing certificate from attestation document 141 | cert = crypto.load_certificate(crypto.FILETYPE_ASN1, doc_obj['certificate']) 142 | # Get the key parameters from the cert public key 143 | cert_public_numbers = cert.get_pubkey().to_cryptography_key().public_numbers() 144 | x = cert_public_numbers.x 145 | y = cert_public_numbers.y 146 | curve = cert_public_numbers.curve 147 | if curve != P384: 148 | Exception("Incorrect curve") 149 | x = long_to_bytes(x) 150 | y = long_to_bytes(y) 151 | 152 | # Create the EC2 key from public key parameters 153 | key = EC2(x = x, y = y, crv = P384) 154 | 155 | 156 | # Get the protected header from attestation document 157 | phdr = cbor2.loads(data[0]) 158 | 159 | # Construct the Sign1 message 160 | msg = Sign1Message(phdr = phdr, uhdr = data[1], payload = doc, key = key) 161 | msg._signature = data[3] 162 | # Verify the signature using the EC2 key 163 | if not msg.verify_signature(): 164 | raise Exception("Wrong signature") 165 | 166 | 167 | ############################################## 168 | # Part 3: Validating signing certificate PKI # 169 | ############################################## 170 | if root_cert_pem is not None: 171 | # Create an X509Store object for the CA bundles 172 | store = crypto.X509Store() 173 | 174 | # Create the CA cert object from PEM string, and store into X509Store 175 | _cert = crypto.load_certificate(crypto.FILETYPE_PEM, root_cert_pem) 176 | store.add_cert(_cert) 177 | 178 | # Get the CA bundle from attestation document and store into X509Store 179 | # Except the first certificate, which is the root certificate 180 | for _cert_binary in doc_obj['cabundle'][1:]: 181 | _cert = crypto.load_certificate(crypto.FILETYPE_ASN1, _cert_binary) 182 | store.add_cert(_cert) 183 | 184 | # add 10 seconds buffer to the current time if the cert expires 185 | if cert.has_expired(): 186 | print(f"Certificate has expired at {cert.get_notAfter().decode('UTF-8')}, adding 10 seconds buffer for verification") 187 | cert.gmtime_adj_notAfter(10) 188 | 189 | # Get the X509Store context 190 | store_ctx = crypto.X509StoreContext(store, cert) 191 | 192 | # Validate the certificate 193 | # If the cert is invalid, it will raise exception 194 | # assuming this checks all items specified in 3.2.3.1. Certificates validity 195 | store_ctx.verify_certificate() 196 | return 197 | if len(sys.argv) < 3: 198 | print("Usage: python verify.py ") 199 | else: 200 | f = open(sys.argv[1], "r") 201 | # from: https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip 202 | r = """-----BEGIN CERTIFICATE----- 203 | MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL 204 | MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD 205 | VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4 206 | MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL 207 | DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG 208 | BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb 209 | 48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE 210 | h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF 211 | R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC 212 | MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW 213 | rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N 214 | IwLz3/Y= 215 | -----END CERTIFICATE-----""" 216 | verify_attestation_doc(base64.b64decode(f.read()), r, sys.argv[2]) 217 | f.close() 218 | -------------------------------------------------------------------------------- /src/chain.rs: -------------------------------------------------------------------------------- 1 | pub mod state; 2 | -------------------------------------------------------------------------------- /src/chain/state.rs: -------------------------------------------------------------------------------- 1 | //! Copyright (c) 2018-2021 Iqlusion Inc. (licensed under the Apache License, Version 2.0) 2 | //! Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 3 | 4 | mod error; 5 | pub use self::error::{StateError, StateErrorDetail}; 6 | pub use tendermint::consensus; 7 | use tendermint::{proposal::SignProposalRequest, vote::SignVoteRequest}; 8 | /// State tracking for double signing prevention 9 | #[derive(Debug, Clone)] 10 | pub struct State { 11 | consensus_state: consensus::State, 12 | } 13 | 14 | /// State persistence over sockets or files 15 | pub trait PersistStateSync { 16 | fn load_state(&mut self) -> Result; 17 | fn persist_state(&mut self, new_state: &consensus::State) -> Result<(), StateError>; 18 | } 19 | 20 | impl State { 21 | /// the underlying consensus state 22 | pub fn consensus_state(&self) -> &consensus::State { 23 | &self.consensus_state 24 | } 25 | 26 | fn check_height(&self, new_state: &consensus::State) -> Result<(), StateError> { 27 | if new_state.height < self.consensus_state.height { 28 | return Err(StateError::height_regression_error( 29 | self.consensus_state.height, 30 | new_state.height, 31 | )); 32 | } 33 | Ok(()) 34 | } 35 | 36 | fn check_round(&self, new_state: &consensus::State) -> Result<(), StateError> { 37 | if new_state.height == self.consensus_state.height 38 | && new_state.round < self.consensus_state.round 39 | { 40 | return Err(StateError::round_regression_error( 41 | new_state.height, 42 | self.consensus_state.round, 43 | new_state.round, 44 | )); 45 | } 46 | Ok(()) 47 | } 48 | 49 | fn check_step(&self, new_state: &consensus::State) -> Result<(), StateError> { 50 | if new_state.height == self.consensus_state.height 51 | && new_state.round == self.consensus_state.round 52 | && new_state.step < self.consensus_state.step 53 | { 54 | return Err(StateError::step_regression_error( 55 | new_state.height, 56 | new_state.round, 57 | self.consensus_state.step, 58 | new_state.step, 59 | )); 60 | } 61 | Ok(()) 62 | } 63 | 64 | fn check_block_id(&self, new_state: &consensus::State) -> Result<(), StateError> { 65 | if new_state.height == self.consensus_state.height 66 | && new_state.round == self.consensus_state.round 67 | && (new_state.block_id != self.consensus_state.block_id && 68 | // disallow voting for two different block IDs during different steps 69 | ((new_state.block_id.is_some() && self.consensus_state.block_id.is_some()) || 70 | // disallow voting `` and for a block ID on the same step 71 | (new_state.step == self.consensus_state.step))) 72 | { 73 | return Err(StateError::double_sign_error( 74 | new_state.height, 75 | new_state.round, 76 | new_state.step, 77 | self.consensus_state.block_id_prefix(), 78 | new_state.block_id_prefix(), 79 | )); 80 | } 81 | Ok(()) 82 | } 83 | 84 | /// Check the chain's height, round, and step 85 | pub fn check_consensus_state(&self, new_state: &consensus::State) -> Result<(), StateError> { 86 | self.check_height(new_state)?; 87 | self.check_round(new_state)?; 88 | self.check_step(new_state)?; 89 | self.check_block_id(new_state) 90 | } 91 | 92 | /// Update the state + check 93 | pub fn check_update_consensus_state( 94 | &mut self, 95 | new_state: consensus::State, 96 | syncer: &mut S, 97 | ) -> Result<(), StateError> { 98 | self.check_consensus_state(&new_state)?; 99 | syncer.persist_state(&new_state)?; 100 | self.consensus_state = new_state; 101 | Ok(()) 102 | } 103 | } 104 | 105 | impl From for State { 106 | fn from(consensus_state: consensus::State) -> Self { 107 | Self { consensus_state } 108 | } 109 | } 110 | 111 | impl From for State { 112 | fn from(req: SignProposalRequest) -> Self { 113 | Self { 114 | consensus_state: consensus::State { 115 | height: req.proposal.height, 116 | round: req.proposal.round, 117 | step: 0, 118 | block_id: req.proposal.block_id, 119 | }, 120 | } 121 | } 122 | } 123 | 124 | impl From for State { 125 | fn from(req: SignVoteRequest) -> Self { 126 | Self { 127 | consensus_state: consensus::State { 128 | height: req.vote.height, 129 | round: req.vote.round, 130 | step: if req.vote.is_precommit() { 2 } else { 1 }, 131 | block_id: req.vote.block_id, 132 | }, 133 | } 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::*; 140 | use tendermint::block; 141 | 142 | const EXAMPLE_BLOCK_ID: &str = 143 | "26C0A41F3243C6BCD7AD2DFF8A8D83A71D29D307B5326C227F734A1A512FE47D"; 144 | 145 | const EXAMPLE_DOUBLE_SIGN_BLOCK_ID: &str = 146 | "2470A41F3243C6BCD7AD2DFF8A8D83A71D29D307B5326C227F734A1A512FE47D"; 147 | 148 | /// Macro for compactly expressing a consensus state 149 | macro_rules! state { 150 | ($height:expr, $round:expr, $step:expr, $block_id:expr) => { 151 | consensus::State { 152 | height: block::Height::from($height as u32), 153 | round: block::Round::from($round as u16), 154 | step: $step, 155 | block_id: $block_id, 156 | } 157 | }; 158 | } 159 | 160 | /// Macro for compactly representing `Some(block_id)` 161 | macro_rules! block_id { 162 | ($id:expr) => { 163 | Some($id.parse::().unwrap()) 164 | }; 165 | } 166 | 167 | /// Macro for creating a test for a successful state update 168 | macro_rules! successful_update_test { 169 | ($name:ident, $old_state:expr, $new_state:expr) => { 170 | #[test] 171 | fn $name() { 172 | State { 173 | consensus_state: $old_state, 174 | } 175 | .check_consensus_state(&$new_state) 176 | .unwrap(); 177 | } 178 | }; 179 | } 180 | 181 | /// Macro for creating a test that expects double sign 182 | macro_rules! double_sign_test { 183 | ($name:ident, $old_state:expr, $new_state:expr) => { 184 | #[test] 185 | fn $name() { 186 | let err = State { 187 | consensus_state: $old_state, 188 | } 189 | .check_consensus_state(&$new_state) 190 | .expect_err("expected StateErrorKind::DoubleSign but succeeded"); 191 | 192 | assert!(matches!( 193 | err, 194 | StateError(StateErrorDetail::DoubleSignError(_), _) 195 | )) 196 | } 197 | }; 198 | } 199 | 200 | successful_update_test!( 201 | height_update_with_nil_block_id_success, 202 | state!(1, 1, 0, None), 203 | state!(2, 0, 0, None) 204 | ); 205 | 206 | successful_update_test!( 207 | step_update_with_nil_to_some_block_id_success, 208 | state!(1, 1, 1, None), 209 | state!(1, 1, 2, block_id!(EXAMPLE_BLOCK_ID)) 210 | ); 211 | 212 | successful_update_test!( 213 | round_update_with_different_block_id_success, 214 | state!(1, 1, 0, block_id!(EXAMPLE_BLOCK_ID)), 215 | state!(2, 0, 0, block_id!(EXAMPLE_DOUBLE_SIGN_BLOCK_ID)) 216 | ); 217 | 218 | successful_update_test!( 219 | round_update_with_block_id_and_nil_success, 220 | state!(1, 1, 0, block_id!(EXAMPLE_BLOCK_ID)), 221 | state!(2, 0, 0, None) 222 | ); 223 | 224 | successful_update_test!( 225 | step_update_with_block_id_and_nil_success, 226 | state!(1, 0, 0, block_id!(EXAMPLE_BLOCK_ID)), 227 | state!(1, 0, 1, None) 228 | ); 229 | 230 | double_sign_test!( 231 | step_update_with_different_block_id_double_sign, 232 | state!(1, 1, 0, block_id!(EXAMPLE_BLOCK_ID)), 233 | state!(1, 1, 1, block_id!(EXAMPLE_DOUBLE_SIGN_BLOCK_ID)) 234 | ); 235 | 236 | double_sign_test!( 237 | same_hrs_with_different_block_id_double_sign, 238 | state!(1, 1, 2, None), 239 | state!(1, 1, 2, block_id!(EXAMPLE_BLOCK_ID)) 240 | ); 241 | } 242 | -------------------------------------------------------------------------------- /src/chain/state/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types regarding chain state (i.e. double signing) 2 | //! Copyright (c) 2018-2021 Iqlusion Inc. (licensed under the Apache License, Version 2.0) 3 | //! Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 4 | 5 | use flex_error::{define_error, DetailOnly}; 6 | use tendermint::block::{Height, Round}; 7 | 8 | define_error! { 9 | StateError { 10 | HeightRegressionError { 11 | last_height: Height, 12 | new_height: Height, 13 | } 14 | |e| { 15 | format_args!("last height:{} new height:{}", e.last_height, e.new_height) 16 | }, 17 | 18 | RoundRegressionError { 19 | height: Height, 20 | last_round: Round, 21 | new_round: Round, 22 | } |e| { 23 | format_args!("round regression at height:{} last round:{} new round:{}", e.height, e.last_round, e.new_round) 24 | }, 25 | 26 | StepRegressionError { 27 | height: Height, 28 | round: Round, 29 | last_step: i8, 30 | new_step: i8, 31 | } |e| { 32 | format_args!("round regression at height:{} round:{} last step:{} new step:{}", e.height, e.round, e.last_step, e.new_step) 33 | }, 34 | DoubleSignError{ 35 | height: Height, 36 | round: Round, 37 | step: i8, 38 | old_block_id: String, 39 | new_block_id: String, 40 | } |e| { 41 | format_args!("Attempting to sign a second proposal at height:{} round:{} step:{} old block id:{} new block {}", e.height, e.round, e.step, e.old_block_id, e.new_block_id) 42 | }, 43 | SyncError{ 44 | path: String, 45 | } [DetailOnly] |e| { 46 | format_args!("Error syncing {}", e.path) 47 | }, 48 | SyncEncDecError{ 49 | path_or_msg: String, 50 | } [DetailOnly] |e| { 51 | format_args!("Error parsing or serializing in syncing {}", e.path_or_msg) 52 | }, 53 | SyncOtherError{ 54 | error_message: String, 55 | } |e| { 56 | format_args!("Error state syncing: {}", e.error_message) 57 | }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | pub mod validator; 2 | -------------------------------------------------------------------------------- /src/config/validator.rs: -------------------------------------------------------------------------------- 1 | //! Validator configuration 2 | //! Copyright (c) 2018-2021 Iqlusion Inc. (licensed under the Apache License, Version 2.0) 3 | //! Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use tendermint::chain; 7 | 8 | /// Validator configuration 9 | #[derive(Clone, Debug, Deserialize, Serialize)] 10 | #[serde(deny_unknown_fields)] 11 | pub struct ValidatorConfig { 12 | /// Chain ID of the Tendermint network this validator is part of 13 | pub chain_id: chain::Id, 14 | 15 | /// Height at which to stop signing 16 | pub max_height: Option, 17 | } 18 | -------------------------------------------------------------------------------- /src/connection.rs: -------------------------------------------------------------------------------- 1 | //! Connections to a validator (TCP/secret or Unix/local-plain socket) 2 | //! Copyright (c) 2018-2021 Iqlusion Inc. (licensed under the Apache License, Version 2.0) 3 | //! Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 4 | 5 | use std::io; 6 | use std::marker::{Send, Sync}; 7 | use tendermint_p2p::secret_connection::SecretConnection; 8 | use tracing::{debug, trace}; 9 | 10 | /// Connections to a validator 11 | pub trait Connection: io::Read + io::Write + Sync + Send {} 12 | 13 | /// Protocol implementation of the plain connection (Unix domain socket or local vsock-proxy) 14 | pub struct PlainConnection { 15 | socket: IoHandler, 16 | } 17 | 18 | impl PlainConnection 19 | where 20 | IoHandler: io::Read + io::Write + Send + Sync, 21 | { 22 | /// Create a new `PlainConnection` for the given socket 23 | pub fn new(socket: IoHandler) -> Self { 24 | Self { socket } 25 | } 26 | } 27 | 28 | impl io::Read for PlainConnection 29 | where 30 | IoHandler: io::Read + io::Write + Send + Sync, 31 | { 32 | fn read(&mut self, data: &mut [u8]) -> Result { 33 | let r = self.socket.read(data); 34 | debug!("read tm connection: {:?}", r); 35 | trace!("read data: {:02X?}", data); 36 | r 37 | } 38 | } 39 | 40 | impl io::Write for PlainConnection 41 | where 42 | IoHandler: io::Read + io::Write + Send + Sync, 43 | { 44 | fn write(&mut self, data: &[u8]) -> Result { 45 | let r = self.socket.write(data); 46 | debug!("write tm connection: {:?}", r); 47 | trace!("written: {:02X?}", data); 48 | r 49 | } 50 | 51 | fn flush(&mut self) -> Result<(), io::Error> { 52 | trace!("flush"); 53 | self.socket.flush() 54 | } 55 | } 56 | 57 | impl Connection for SecretConnection where T: io::Read + io::Write + Sync + Send {} 58 | impl Connection for PlainConnection where T: io::Read + io::Write + Sync + Send {} 59 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | //! Copyright (c) 2018-2021 Iqlusion Inc. (licensed under the Apache License, Version 2.0) 3 | //! Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 4 | 5 | use flex_error::define_error; 6 | use flex_error::DetailOnly; 7 | 8 | define_error! { 9 | Error { 10 | SigningTendermintError { error: String } 11 | [ DetailOnly ] 12 | |e| { 13 | e.error.clone() 14 | }, 15 | SigningStateError { error: String } 16 | [ DetailOnly ] 17 | |e| { 18 | e.error.clone() 19 | }, 20 | AccessError { 21 | } |_| { 22 | "Access Denied" 23 | }, 24 | 25 | ChainIdError { 26 | chain_id: String, 27 | } |e| { 28 | format_args!("chain ID error: {}", e.chain_id) 29 | }, 30 | 31 | DoubleSign { 32 | } |_| { 33 | "Attempted double sign" 34 | }, 35 | 36 | ExceedMaxHeight { 37 | request_height: i64, 38 | max_height: u64, 39 | } |e| { 40 | format_args!("attempted to sign at height {} which is greater than {}", e.request_height, e.max_height) 41 | }, 42 | 43 | InvalidKeyError { 44 | } |_| { 45 | "invalid key" 46 | }, 47 | 48 | IoError { error: String } 49 | [ DetailOnly ] 50 | |e| { 51 | e.error.clone() 52 | }, 53 | 54 | PanicError { 55 | } |_| { 56 | "internal crash" 57 | }, 58 | 59 | ProtocolError { error: String } 60 | [ DetailOnly ] 61 | |e| { 62 | e.error.clone() 63 | }, 64 | 65 | ProtocolErrorTendermint { error: String } 66 | [ DetailOnly ] 67 | |e| { 68 | e.error.clone() 69 | }, 70 | 71 | ProtocolErrorMsg { error: String } 72 | [ DetailOnly> ] 73 | |e| { 74 | e.error.clone() 75 | }, 76 | 77 | SerializationError { 78 | } [ DetailOnly ] |e| { 79 | format_args!("serialization error: {}", e) 80 | }, 81 | 82 | } 83 | } 84 | 85 | /// Wraps IO-related error from a different source into an IO error 86 | /// as a kind Other 87 | pub fn io_error_wrap>>( 88 | message: String, 89 | error: E, 90 | ) -> Error { 91 | Error::io_error( 92 | message, 93 | std::io::Error::new(std::io::ErrorKind::Other, error), 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod chain; 2 | pub mod config; 3 | pub mod connection; 4 | pub mod error; 5 | mod rpc; 6 | pub mod session; 7 | pub mod utils; 8 | -------------------------------------------------------------------------------- /src/rpc.rs: -------------------------------------------------------------------------------- 1 | //! Remote Procedure Calls 2 | //! Copyright (c) 2018-2021 Iqlusion Inc. (licensed under the Apache License, Version 2.0) 3 | //! Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 4 | 5 | use crate::error::Error; 6 | use prost::Message as _; 7 | use std::convert::TryFrom; 8 | use std::io::Read; 9 | use tendermint::proposal::{SignProposalRequest, SignedProposalResponse}; 10 | use tendermint::public_key::{PubKeyRequest, PublicKey}; 11 | use tendermint::vote::{SignVoteRequest, SignedVoteResponse}; 12 | use tendermint_p2p::secret_connection::DATA_MAX_SIZE; 13 | use tendermint_proto::{ 14 | crypto::{public_key::Sum as PkSum, PublicKey as RawPublicKey}, 15 | privval::{ 16 | message::Sum, Message as PrivMessage, PingRequest, PingResponse, PubKeyResponse, 17 | RemoteSignerError, SignedProposalResponse as RawProposalResponse, 18 | SignedVoteResponse as RawVoteResponse, 19 | }, 20 | }; 21 | 22 | /// Requests to the KMS 23 | #[derive(Debug)] 24 | pub enum Request { 25 | /// Sign the given message 26 | SignProposal(SignProposalRequest), 27 | SignVote(SignVoteRequest), 28 | ShowPublicKey(PubKeyRequest), 29 | 30 | // PingRequest is a PrivValidatorSocket message to keep the connection alive. 31 | ReplyPing(PingRequest), 32 | } 33 | 34 | impl Request { 35 | /// Read a request from the given readable 36 | pub fn read(conn: &mut impl Read) -> Result { 37 | let msg = read_msg(conn)?; 38 | 39 | // Parse Protobuf-encoded request message 40 | let msg = PrivMessage::decode_length_delimited(msg.as_ref()) 41 | .map_err(|e| Error::protocol_error("malformed message packet".into(), e.into()))? 42 | .sum; 43 | 44 | match msg { 45 | Some(Sum::SignVoteRequest(req)) => { 46 | let svr = SignVoteRequest::try_from(req).map_err(|e| { 47 | Error::protocol_error_tendermint( 48 | "sign vote request domain type error".into(), 49 | e, 50 | ) 51 | })?; 52 | Ok(Request::SignVote(svr)) 53 | } 54 | Some(Sum::SignProposalRequest(spr)) => { 55 | let spr = SignProposalRequest::try_from(spr).map_err(|e| { 56 | Error::protocol_error_tendermint( 57 | "sign proposal request domain type error".into(), 58 | e, 59 | ) 60 | })?; 61 | Ok(Request::SignProposal(spr)) 62 | } 63 | Some(Sum::PubKeyRequest(pkr)) => { 64 | let pkr = PubKeyRequest::try_from(pkr).map_err(|e| { 65 | Error::protocol_error_tendermint("pubkey request domain type error".into(), e) 66 | })?; 67 | Ok(Request::ShowPublicKey(pkr)) 68 | } 69 | Some(Sum::PingRequest(pr)) => Ok(Request::ReplyPing(pr)), 70 | _ => Err(Error::protocol_error_msg("invalid RPC message".into(), msg)), 71 | } 72 | } 73 | } 74 | 75 | /// Responses from the KMS 76 | #[derive(Debug)] 77 | pub enum Response { 78 | /// Signature response 79 | SignedVote(SignedVoteResponse), 80 | SignedVoteError(RemoteSignerError), 81 | SignedProposal(SignedProposalResponse), 82 | SignedProposalError(RemoteSignerError), 83 | Ping(PingResponse), 84 | PublicKey(PublicKey), 85 | PublicKeyError(RemoteSignerError), 86 | } 87 | 88 | /// possible options for double signing error 89 | pub enum DoubleSignErrorType { 90 | Vote, 91 | Proposal, 92 | } 93 | 94 | /// possible options for chain id error 95 | pub enum ChainIdErrorType { 96 | Pubkey, 97 | Vote, 98 | Proposal, 99 | } 100 | 101 | impl Response { 102 | /// signed vote 103 | pub fn vote_response(vote: SignVoteRequest, signature: ed25519_consensus::Signature) -> Self { 104 | let mut vote = vote.vote; 105 | vote.signature = Some(signature.into()); 106 | Response::SignedVote(SignedVoteResponse { 107 | vote: Some(vote), 108 | error: None, 109 | }) 110 | } 111 | 112 | /// signed proposal 113 | pub fn proposal_response( 114 | proposal: SignProposalRequest, 115 | signature: ed25519_consensus::Signature, 116 | ) -> Self { 117 | let mut proposal = proposal.proposal; 118 | proposal.signature = Some(signature.into()); 119 | Response::SignedProposal(SignedProposalResponse { 120 | proposal: Some(proposal), 121 | error: None, 122 | }) 123 | } 124 | 125 | /// double signing error 126 | pub fn double_sign(req_type: DoubleSignErrorType, height: i64) -> Self { 127 | let error = RemoteSignerError { 128 | code: 2, 129 | description: format!("double signing requested at height: {}", height), 130 | }; 131 | match req_type { 132 | DoubleSignErrorType::Vote => Self::SignedVoteError(error), 133 | DoubleSignErrorType::Proposal => Self::SignedProposalError(error), 134 | } 135 | } 136 | 137 | /// invalid chain id error 138 | pub fn invalid_chain_id(req_type: ChainIdErrorType, chain_id: &tendermint::chain::Id) -> Self { 139 | let error = RemoteSignerError { 140 | code: 1, 141 | description: format!("invalid chain id: {}", chain_id), 142 | }; 143 | match req_type { 144 | ChainIdErrorType::Vote => Self::SignedVoteError(error), 145 | ChainIdErrorType::Proposal => Self::SignedProposalError(error), 146 | ChainIdErrorType::Pubkey => Self::PublicKeyError(error), 147 | } 148 | } 149 | 150 | /// Encode response to bytes 151 | pub fn encode(self) -> Result, Error> { 152 | let mut buf = Vec::new(); 153 | 154 | let msg = match self { 155 | Response::SignedVote(resp) => Sum::SignedVoteResponse(resp.into()), 156 | Response::SignedProposal(resp) => Sum::SignedProposalResponse(resp.into()), 157 | Response::Ping(_) => Sum::PingResponse(PingResponse {}), 158 | Response::PublicKey(pk) => { 159 | let pkr = PubKeyResponse { 160 | pub_key: Some(RawPublicKey { 161 | sum: Some(PkSum::Ed25519(pk.to_bytes())), 162 | }), 163 | error: None, 164 | }; 165 | Sum::PubKeyResponse(pkr) 166 | } 167 | Response::SignedVoteError(error) => Sum::SignedVoteResponse(RawVoteResponse { 168 | vote: None, 169 | error: Some(error), 170 | }), 171 | Response::SignedProposalError(error) => { 172 | Sum::SignedProposalResponse(RawProposalResponse { 173 | proposal: None, 174 | error: Some(error), 175 | }) 176 | } 177 | Response::PublicKeyError(error) => Sum::PubKeyResponse(PubKeyResponse { 178 | pub_key: None, 179 | error: Some(error), 180 | }), 181 | }; 182 | 183 | PrivMessage { sum: Some(msg) } 184 | .encode_length_delimited(&mut buf) 185 | .map_err(|e| Error::protocol_error("failed to encode response".into(), e.into()))?; 186 | Ok(buf) 187 | } 188 | } 189 | 190 | /// Read a message from a Secret Connection 191 | // TODO(tarcieri): extract this into Secret Connection 192 | fn read_msg(conn: &mut impl Read) -> Result, Error> { 193 | let mut buf = vec![0; DATA_MAX_SIZE]; 194 | let buf_read = conn 195 | .read(&mut buf) 196 | .map_err(|e| Error::io_error("read msg failed".into(), e))?; 197 | buf.truncate(buf_read); 198 | Ok(buf) 199 | } 200 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | //! Copyright (c) 2018-2021 Iqlusion Inc. (licensed under the Apache License, Version 2.0) 2 | //! Modifications Copyright (c) 2021-present Crypto.com (licensed under the Apache License, Version 2.0) 3 | 4 | use crate::{ 5 | chain::state::{PersistStateSync, State, StateError, StateErrorDetail}, 6 | config::validator::ValidatorConfig, 7 | connection::Connection, 8 | error::Error, 9 | rpc::{ChainIdErrorType, DoubleSignErrorType, Request, Response}, 10 | }; 11 | use ed25519_consensus::SigningKey; 12 | use std::time::Instant; 13 | use tendermint_proto::privval::PingResponse; 14 | use tracing::{debug, error, info}; 15 | 16 | /// Encrypted or plain session with a validator node 17 | pub struct Session { 18 | /// Validator configuration options 19 | config: ValidatorConfig, 20 | 21 | /// connection to a validator node 22 | connection: Box, 23 | 24 | /// consensus signing key 25 | signing_key: SigningKey, 26 | 27 | /// consensus state 28 | state: State, 29 | 30 | /// consensus state persistence 31 | state_syncer: S, 32 | } 33 | 34 | impl Session { 35 | pub fn reset_connection(&mut self, connection: Box) { 36 | self.connection = connection; 37 | } 38 | 39 | pub fn new( 40 | config: ValidatorConfig, 41 | connection: Box, 42 | signing_key: SigningKey, 43 | state: State, 44 | state_syncer: S, 45 | ) -> Self { 46 | Self { 47 | config, 48 | connection, 49 | signing_key, 50 | state, 51 | state_syncer, 52 | } 53 | } 54 | 55 | /// Check chain id matches the configured one 56 | fn check_chain_id(&self, chain_id: &tendermint::chain::Id) -> Result<(), Error> { 57 | if chain_id == &self.config.chain_id { 58 | Ok(()) 59 | } else { 60 | Err(Error::chain_id_error(chain_id.to_string())) 61 | } 62 | } 63 | 64 | /// If a max block height is configured, ensure the block we're signing 65 | /// doesn't exceed it 66 | fn check_max_height(&self, request_height: i64) -> Result<(), Error> { 67 | if let Some(max_height) = self.config.max_height { 68 | if request_height > max_height.value() as i64 { 69 | return Err(Error::exceed_max_height(request_height, max_height.into())); 70 | } 71 | } 72 | Ok(()) 73 | } 74 | 75 | /// Main request loop 76 | pub fn request_loop(&mut self) -> Result<(), Error> { 77 | while self.handle_request()? {} 78 | Ok(()) 79 | } 80 | 81 | /// Handle an incoming request from the validator 82 | fn handle_request(&mut self) -> Result { 83 | let request = Request::read(&mut self.connection)?; 84 | debug!( 85 | "[{}] received request: {:?}", 86 | &self.config.chain_id, &request 87 | ); 88 | let response = match request { 89 | Request::SignProposal(req) => { 90 | if self.check_chain_id(&req.chain_id).is_err() { 91 | Response::invalid_chain_id(ChainIdErrorType::Proposal, &req.chain_id) 92 | } else { 93 | self.check_max_height(req.proposal.height.into())?; 94 | let request_state = State::from(req.clone()); 95 | let req_cs = request_state.consensus_state(); 96 | match self 97 | .state 98 | .check_update_consensus_state(req_cs.clone(), &mut self.state_syncer) 99 | { 100 | Ok(_) => { 101 | let signable_bytes = req.to_signable_vec().map_err(|e| { 102 | Error::signing_tendermint_error( 103 | "can't get proposal signable bytes".into(), 104 | e, 105 | ) 106 | })?; 107 | let started_at = Instant::now(); 108 | let signature = self.signing_key.sign(&signable_bytes); 109 | info!( 110 | "[{}] signed:{} at h/r/s {} ({} ms)", 111 | &self.config.chain_id, 112 | req_cs.block_id_prefix(), 113 | req_cs, 114 | started_at.elapsed().as_millis(), 115 | ); 116 | Response::proposal_response(req, signature) 117 | } 118 | Err(StateError(StateErrorDetail::DoubleSignError(_), _)) => { 119 | // Report double signing error back to the validator 120 | let original_block_id = self.state.consensus_state().block_id_prefix(); 121 | 122 | error!( 123 | "[{}] attempted double sign at h/r/s: {} ({} != {})", 124 | &self.config.chain_id, 125 | req_cs, 126 | original_block_id, 127 | req_cs.block_id_prefix() 128 | ); 129 | 130 | Response::double_sign( 131 | DoubleSignErrorType::Proposal, 132 | req_cs.height.into(), 133 | ) 134 | } 135 | Err(e) => { 136 | return Err(Error::signing_state_error( 137 | "failed signing proposal".into(), 138 | e, 139 | )) 140 | } 141 | } 142 | } 143 | } 144 | Request::SignVote(req) => { 145 | if self.check_chain_id(&req.chain_id).is_err() { 146 | Response::invalid_chain_id(ChainIdErrorType::Vote, &req.chain_id) 147 | } else { 148 | self.check_max_height(req.vote.height.into())?; 149 | let request_state = State::from(req.clone()); 150 | let req_cs = request_state.consensus_state(); 151 | match self 152 | .state 153 | .check_update_consensus_state(req_cs.clone(), &mut self.state_syncer) 154 | { 155 | Ok(_) => { 156 | let signable_bytes = req.to_signable_vec().map_err(|e| { 157 | Error::signing_tendermint_error( 158 | "cannot get vote signable bytes".into(), 159 | e, 160 | ) 161 | })?; 162 | let started_at = Instant::now(); 163 | let signature = self.signing_key.sign(&signable_bytes); 164 | info!( 165 | "[{}] signed:{} at h/r/s {} ({} ms)", 166 | &self.config.chain_id, 167 | req_cs.block_id_prefix(), 168 | req_cs, 169 | started_at.elapsed().as_millis(), 170 | ); 171 | Response::vote_response(req, signature) 172 | } 173 | Err(StateError(StateErrorDetail::DoubleSignError(_), _)) => { 174 | // Report double signing error back to the validator 175 | let original_block_id = self.state.consensus_state().block_id_prefix(); 176 | 177 | error!( 178 | "[{}] attempted double sign at h/r/s: {} ({} != {})", 179 | &self.config.chain_id, 180 | req_cs, 181 | original_block_id, 182 | req_cs.block_id_prefix() 183 | ); 184 | 185 | Response::double_sign(DoubleSignErrorType::Vote, req_cs.height.into()) 186 | } 187 | Err(e) => { 188 | return Err(Error::signing_state_error("failed signing vote".into(), e)) 189 | } 190 | } 191 | } 192 | } 193 | // non-signable requests: 194 | Request::ReplyPing(_) => Response::Ping(PingResponse {}), 195 | Request::ShowPublicKey(ref req) => { 196 | if self.check_chain_id(&req.chain_id).is_err() { 197 | Response::invalid_chain_id(ChainIdErrorType::Pubkey, &req.chain_id) 198 | } else { 199 | let pubkey = self.signing_key.verification_key().to_bytes(); 200 | Response::PublicKey( 201 | tendermint::PublicKey::from_raw_ed25519(&pubkey).expect("public key"), 202 | ) 203 | } 204 | } 205 | }; 206 | debug!( 207 | "[{}] sending response: {:?}", 208 | &self.config.chain_id, &response 209 | ); 210 | 211 | let response_bytes = response.encode()?; 212 | self.connection 213 | .write_all(&response_bytes) 214 | .map_err(|e| Error::io_error("write response failed".into(), e))?; 215 | 216 | Ok(true) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, Write}; 2 | use std::str::FromStr; 3 | use tracing::{debug, trace}; 4 | 5 | use crate::error::Error; 6 | 7 | /// Options for displaying public key 8 | #[derive(Debug, Clone, Copy)] 9 | pub enum PubkeyDisplay { 10 | Base64, 11 | Bech32, 12 | } 13 | 14 | impl FromStr for PubkeyDisplay { 15 | type Err = String; 16 | 17 | fn from_str(s: &str) -> Result { 18 | match s { 19 | "base64" => Ok(PubkeyDisplay::Base64), 20 | "bech32" => Ok(PubkeyDisplay::Bech32), 21 | _ => Err("unknown display type".to_owned()), 22 | } 23 | } 24 | } 25 | 26 | /// prints public key in the desired format 27 | pub fn print_pubkey( 28 | bech32_prefix: Option, 29 | ptype: Option, 30 | public: ed25519_consensus::VerificationKey, 31 | ) { 32 | match ptype { 33 | Some(PubkeyDisplay::Bech32) => { 34 | let prefix = bech32_prefix.unwrap_or_else(|| "cosmosvalconspub".to_owned()); 35 | let mut data = vec![0x16, 0x24, 0xDE, 0x64, 0x20]; 36 | data.extend_from_slice(public.as_bytes()); 37 | println!( 38 | "public key: {}", 39 | subtle_encoding::bech32::encode(prefix, data) 40 | ); 41 | } 42 | _ => { 43 | println!( 44 | "public key: {}", 45 | String::from_utf8(subtle_encoding::base64::encode(public)).unwrap() 46 | ); 47 | let pk = tendermint::public_key::Ed25519::try_from(&public.as_bytes()[..]) 48 | .expect("public key"); 49 | let id = tendermint::node::Id::from(pk); 50 | println!("address: {}", id); 51 | } 52 | } 53 | } 54 | 55 | /// Read u16-size payload (for vsock) 56 | pub fn read_u16_payload(stream: &mut S) -> Result, Error> { 57 | let mut len_b = [0u8; 2]; 58 | stream 59 | .read_exact(&mut len_b) 60 | .map_err(|e| Error::io_error("Error reading length".to_owned(), e))?; 61 | 62 | let l = (u16::from_le_bytes(len_b)) as usize; 63 | if l > 0 { 64 | let mut state_raw = vec![0u8; l]; 65 | let mut total = 0; 66 | 67 | while let Ok(n) = stream.read(&mut state_raw[total..]) { 68 | total += n; 69 | // no more data to read 70 | if n == 0 || total >= l { 71 | break; 72 | } 73 | } 74 | 75 | if total == 0 { 76 | return Err(Error::io_error( 77 | "Zero length".to_owned(), 78 | std::io::Error::from(std::io::ErrorKind::UnexpectedEof), 79 | )); 80 | } 81 | state_raw.resize(total, 0); 82 | Ok(state_raw) 83 | } else { 84 | trace!("read empty payload"); 85 | Ok(Vec::default()) 86 | } 87 | } 88 | 89 | /// Write u16-sized payload (for vsock) 90 | pub fn write_u16_payload(stream: &mut S, data: &[u8]) -> io::Result<()> { 91 | if data.len() > u16::MAX as usize { 92 | return Err(io::ErrorKind::InvalidInput.into()); 93 | } 94 | debug!("writing u16-sized payload"); 95 | let data_len = (data.len() as u16).to_le_bytes(); 96 | 97 | stream.write_all(&data_len)?; 98 | stream.write_all(data)?; 99 | stream.flush()?; 100 | debug!("successfully wrote u16-sized payload"); 101 | Ok(()) 102 | } 103 | --------------------------------------------------------------------------------