diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 00000000000..211ea0c2102 --- /dev/null +++ b/.bazelignore @@ -0,0 +1,3 @@ +# examples has its own WORKSPACE. Ignore as part of this root WORKSPACE so that +# we don't need to repeat dependencies. +examples/ diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 00000000000..53485cb9743 --- /dev/null +++ b/.bazelrc @@ -0,0 +1 @@ +build --cxxopt=-std=c++17 --host_cxxopt=-std=c++17 diff --git a/.gitattributes b/.gitattributes index 6754557eab6..4343b9bd8ec 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ -TestService.java.txt binary -TestServiceLite.java.txt binary -TestServiceNano.java.txt binary +TestService.java.txt -text +TestServiceLite.java.txt -text +TestDeprecatedService.java.txt -text +TestDeprecatedServiceLite.java.txt -text diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE deleted file mode 100644 index 5ffc3a7a9f1..00000000000 --- a/.github/ISSUE_TEMPLATE +++ /dev/null @@ -1,17 +0,0 @@ -Please answer these questions before submitting your issue. - -### What version of gRPC are you using? - - -### What JVM are you using (`java -version`)? - - -### What did you do? -If possible, provide a recipe for reproducing the error. - - -### What did you expect to see? - - -### What did you see instead? - diff --git a/.github/ISSUE_TEMPLATE/ask_question.md b/.github/ISSUE_TEMPLATE/ask_question.md new file mode 100644 index 00000000000..c384031e347 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask_question.md @@ -0,0 +1,12 @@ +--- +name: Ask a question +about: Asking a question related gRPC-Java +labels: question +--- + + + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..b6998dde7c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,19 @@ +--- +name: Report a bug +about: Create a bug report to help us improve +--- + + + +### What version of gRPC-Java are you using? + +### What is your environment? + + +### What did you expect to see? + +### What did you see instead? + +### Steps to reproduce the bug + + diff --git a/.github/ISSUE_TEMPLATE/feature_report.md b/.github/ISSUE_TEMPLATE/feature_report.md new file mode 100644 index 00000000000..ddc60838148 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_report.md @@ -0,0 +1,19 @@ +--- +name: Request a feature +about: Suggest an enhancement for gRPC +labels: enhancement +--- + + + +### Is your feature request related to a problem? + + +### Describe the solution you'd like + + +### Describe alternatives you've considered + + +### Additional context + diff --git a/.github/workflows/branch-testing.yml b/.github/workflows/branch-testing.yml new file mode 100644 index 00000000000..ece8ec4cd58 --- /dev/null +++ b/.github/workflows/branch-testing.yml @@ -0,0 +1,41 @@ +name: GitHub Actions Branch Testing + +on: + push: + branches: + - master + - 'v1.*' + schedule: + - cron: '54 19 * * SUN' # weekly at a "random" time + +permissions: + contents: read + +jobs: + arm64: + runs-on: ubuntu-24.04-arm + strategy: + matrix: + jre: [17] + fail-fast: false # Should swap to true if we grow a large matrix + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.jre }} + distribution: 'temurin' + + - name: Gradle cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build + run: ./gradlew -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs='-Xmx1g' -PskipAndroid=true -PskipCodegen=true -PerrorProne=false test + diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 00000000000..da1e2fed114 --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,13 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +permissions: + contents: read + +jobs: + validation: + name: "Gradle wrapper validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@v4 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 00000000000..3070a1a2f7c --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,20 @@ +name: 'Lock Threads' + +on: + workflow_dispatch: + schedule: + - cron: '37 3 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + github-token: ${{ github.token }} + issue-inactive-days: 90 + pr-inactive-days: 90 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000000..ccabd9be79f --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,110 @@ +name: GitHub Actions Linux Testing + +on: + push: + branches: + - master + - 'v1.*' + pull_request: + schedule: + - cron: '54 19 * * SUN' # weekly at a "random" time + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + jre: [8, 11, 17, 21] + fail-fast: false # Should swap to true if we grow a large matrix + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.jre }} + distribution: 'temurin' + + - name: Gradle cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Maven cache + uses: actions/cache@v4 + with: + path: | + ~/.m2/repository + !~/.m2/repository/io/grpc + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml', 'build.gradle') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Protobuf cache + uses: actions/cache@v4 + with: + path: /tmp/protobuf-cache + key: ${{ runner.os }}-maven-${{ hashFiles('buildscripts/make_dependencies.sh') }} + + - name: Build + run: buildscripts/kokoro/unix.sh + - name: Post Failure Upload Test Reports to Artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: Test Reports (JRE ${{ matrix.jre }}) + path: | + ./*/build/reports/tests/** + ./*/*/build/reports/tests/** + retention-days: 14 + - name: Check for modified codegen + run: test -z "$(git status --porcelain)" || (git status && echo Error Working directory is not clean. Forget to commit generated files? && false) + + - name: Coveralls + if: matrix.jre == 8 # Upload once, instead of for each job in the matrix + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: ./gradlew :grpc-all:coveralls -PskipAndroid=true -x compileJava + - name: Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + bazel: + runs-on: ubuntu-latest + strategy: + matrix: + bzlmod: [true, false] + env: + USE_BAZEL_VERSION: 7.7.1 + + steps: + - uses: actions/checkout@v4 + + - name: Check versions match in MODULE.bazel and repositories.bzl + run: | + diff -u <(sed -n '/GRPC_DEPS_START/,/GRPC_DEPS_END/ {/GRPC_DEPS_/! p}' MODULE.bazel) \ + <(sed -n '/GRPC_DEPS_START/,/GRPC_DEPS_END/ {/GRPC_DEPS_/! p}' repositories.bzl) + + - name: Bazel cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/bazel/*/cache + ~/.cache/bazelisk/downloads + key: ${{ runner.os }}-bazel-${{ env.USE_BAZEL_VERSION }}-${{ hashFiles('WORKSPACE', 'repositories.bzl') }} + + - name: Run bazel build + run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }} + + - name: Run bazel test + run: bazelisk test //... --enable_bzlmod=${{ matrix.bzlmod }} + + - name: Run example bazel build + run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }} + working-directory: ./examples diff --git a/.gitignore b/.gitignore index eb0afe5c13a..b078d891adf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,26 @@ build gradle.properties .gradle local.properties +out # Maven (examples) target +# Bazel +bazel-bin +bazel-examples +bazel-genfiles +bazel-grpc-java +bazel-out +bazel-testlogs +MODULE.bazel.lock + # IntelliJ IDEA .idea *.iml +*.ipr +*.iws +.ijwb # Eclipse .classpath @@ -18,9 +31,18 @@ target .gitignore bin +# VsCode +.vscode + # OS X .DS_Store # Emacs *~ \#*\# + +# ARM tests +qemu-arm-static + +# Temporary output dir for artifacts +mvn-artifacts diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 30227741a5a..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -sudo: false - -language: java - -env: - global: - - GRADLE_OPTS=-Xmx512m - - PROTOBUF_VERSION=3.2.0 - - LDFLAGS=-L/tmp/protobuf/lib - - CXXFLAGS=-I/tmp/protobuf/include - - LD_LIBRARY_PATH=/tmp/protobuf/lib - -before_install: - - mkdir -p $HOME/.gradle/caches && - ln -s /tmp/gradle-caches-modules-2 $HOME/.gradle/caches/modules-2 - - mkdir -p $HOME/.gradle && - ln -s /tmp/gradle-wrapper $HOME/.gradle/wrapper - # Work around https://github.com/travis-ci/travis-ci/issues/2317 - - if \[ "$TRAVIS_OS_NAME" = linux \]; then jdk_switcher use oraclejdk8; fi - - buildscripts/make_dependencies.sh # build protoc into /tmp/protobuf-${PROTOBUF_VERSION} - - ln -s "/tmp/protobuf-${PROTOBUF_VERSION}/$(uname -s)-$(uname -p)" /tmp/protobuf - - mkdir -p $HOME/.gradle - - echo "checkstyle.ignoreFailures=false" >> $HOME/.gradle/gradle.properties - - echo "failOnWarnings=true" >> $HOME/.gradle/gradle.properties - -install: - - ./gradlew assemble generateTestProto install - - pushd examples && ./gradlew build && popd - - pushd examples && mvn verify && popd - -before_script: - - test -z "$(git status --porcelain)" || (git status && echo Error Working directory is not clean. Forget to commit generated files? && false) - -script: - - ./gradlew check :grpc-all:jacocoTestReport - -after_success: - - if \[ "$TRAVIS_OS_NAME" = linux \]; then ./gradlew :grpc-all:coveralls; fi - - bash <(curl -s https://codecov.io/bash) - -os: - - linux - - osx - -notifications: - email: false - -cache: - directories: - - /tmp/protobuf-${PROTOBUF_VERSION} - - /tmp/gradle-caches-modules-2 - - /tmp/gradle-wrapper - -before_cache: - # The lock changes based on folder name; normally $HOME/.gradle/caches/modules-2/modules-2.lock - - rm /tmp/gradle-caches-modules-2/gradle-caches-modules-2.lock - - find $HOME/.gradle/wrapper -not -name "*-all.zip" -and -not -name "*-bin.zip" -delete diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000000..e491a9e7f78 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Google Inc. diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 00000000000..27a99fb62eb --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,71 @@ +# Copyright 2017 The gRPC Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@rules_java//java:defs.bzl", "java_library", "java_plugin") +load("@rules_jvm_external//:defs.bzl", "artifact") +load(":java_grpc_library.bzl", "java_grpc_library") + +java_proto_library( + name = "api_proto_java", + deps = ["@com_google_protobuf//:api_proto"], +) + +java_grpc_library( + name = "java_grpc_library__external_repo_test", + srcs = ["@com_google_protobuf//:api_proto"], + deps = [":api_proto_java"], +) + +java_library( + name = "java_grpc_library_deps__do_not_reference", + visibility = ["//visibility:public"], + exports = [ + "//api", + "//protobuf", + "//stub", + "@com_google_protobuf//:protobuf_java", + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.guava:guava"), + ], +) + +java_library( + name = "java_lite_grpc_library_deps__do_not_reference", + visibility = ["//visibility:public"], + exports = [ + "//api", + "//protobuf-lite", + "//stub", + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.guava:guava"), + ], +) + +java_plugin( + name = "auto_value", + generates_api = 1, + processor_class = "com.google.auto.value.processor.AutoValueProcessor", + deps = [artifact("com.google.auto.value:auto-value")], +) + +java_library( + name = "auto_value_annotations", + exported_plugins = [":auto_value"], + neverlink = 1, + visibility = ["//:__subpackages__"], + exports = [ + artifact("com.google.auto.value:auto-value-annotations"), + ], +) diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100644 index d38ae8e7501..00000000000 --- a/CHANGES.md +++ /dev/null @@ -1,11 +0,0 @@ -Changes between 0.9.0 and 0.10.0: --------------------------------- - -#### Features - -#### API Changes -* OkHttpChannelBuilder.overrideHostForAuthority is deprecated - -#### Bug Fixes -* ServerCall forces headers to be sent first -* Servers cannot be started after shutting down (#1023) diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 00000000000..9d4213ebca7 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,3 @@ +## Community Code of Conduct + +gRPC follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/COMPILING.md b/COMPILING.md index 03b807be269..b7df1319beb 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -1,15 +1,22 @@ Building gRPC-Java ================== -Building is only necessary if you are making changes to gRPC-Java. +Building is only necessary if you are making changes to gRPC-Java or testing/using a non-released + version (e.g. master HEAD) of gRPC-Java library. Building requires JDK 8, as our tests use TLS. grpc-java has a C++ code generation plugin for protoc. Since many Java -developers don't have C compilers installed and don't need to modify the +developers don't have C compilers installed and don't need to run or modify the codegen, the build can skip it. To skip, create the file `/gradle.properties` and add `skipCodegen=true`. +Some parts of grpc-java depend on Android. Since many Java developers don't have +the Android SDK installed and don't need to run or modify the Android +components, the build can skip it. To skip, create the file +`/gradle.properties` and add `skipAndroid=true`. +Otherwise, create the file `/gradle.properties` and add `android.useAndroidX=true`. + Then, to build, run: ``` $ ./gradlew build @@ -18,7 +25,17 @@ $ ./gradlew build To install the artifacts to your Maven local repository for use in your own project, run: ``` -$ ./gradlew install +$ ./gradlew publishToMavenLocal +``` + +### Notes for IntelliJ +Building in IntelliJ works best when you import the project as a Gradle project and delegate IDE +build/run actions to Gradle. + +You can find this setting at: +```Settings -> Build, Execution, Deployment + -> Build Tools -> Gradle -> Runner + -> Delegate IDE build/run actions to gradle. ``` How to Build Code Generation Plugin @@ -27,23 +44,22 @@ This section is only necessary if you are making changes to the code generation. Most users only need to use `skipCodegen=true` as discussed above. ### Build Protobuf -The codegen plugin is C++ code and requires protobuf 3.0.0 or later. +The codegen plugin is C++ code and requires protobuf 22.5 or later. For Linux, Mac and MinGW: ``` -$ git clone https://github.com/google/protobuf.git -$ cd protobuf -$ git checkout v3.2.0 -$ ./autogen.sh -$ ./configure -$ make -$ make check +$ PROTOBUF_VERSION=22.5 +$ curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VERSION/protobuf-all-$PROTOBUF_VERSION.tar.gz +$ tar xzf protobuf-all-$PROTOBUF_VERSION.tar.gz +$ cd protobuf-$PROTOBUF_VERSION +$ ./configure --disable-shared +$ make # You may want to pass -j to make this run faster; see make --help $ sudo make install ``` If you are comfortable with C++ compilation and autotools, you can specify a ``--prefix`` for Protobuf and use ``-I`` in ``CXXFLAGS``, ``-L`` in -``LDFLAGS``, ``LD_LIBRARY_PATH``, and ``PATH`` to reference it. The +``LDFLAGS`` to reference it. The environment variables will be used when building grpc-java. Protobuf installs to ``/usr/local`` by default. @@ -51,15 +67,8 @@ Protobuf installs to ``/usr/local`` by default. For Visual C++, please refer to the [Protobuf README](https://github.com/google/protobuf/blob/master/cmake/README.md) for how to compile Protobuf. gRPC-java assumes a Release build. -#### Linux and MinGW -If ``/usr/local/lib`` is not in your library search path, you can add it by running: -``` -$ sudo sh -c 'echo /usr/local/lib >> /etc/ld.so.conf' -$ sudo ldconfig -``` - #### Mac -Some versions of Mac OS X (e.g., 10.10) doesn't have ``/usr/local`` in the +Some versions of Mac OS X (e.g., 10.10) don't have ``/usr/local`` in the default search paths for header files and libraries. It will fail the build of the codegen. To work around this, you will need to set environment variables: ``` @@ -71,17 +80,17 @@ $ export CXXFLAGS="-I/usr/local/include" LDFLAGS="-L/usr/local/lib" When building on Windows and VC++, you need to specify project properties for Gradle to find protobuf: ``` -.\gradlew install ^ - -PvcProtobufInclude=C:\path\to\protobuf-3.2.0\src ^ - -PvcProtobufLibs=C:\path\to\protobuf-3.2.0\vsprojects\Release ^ +.\gradlew publishToMavenLocal ^ + -PvcProtobufInclude=C:\path\to\protobuf\src ^ + -PvcProtobufLibs=C:\path\to\protobuf\vsprojects\Release ^ -PtargetArch=x86_32 ``` Since specifying those properties every build is bothersome, you can instead create ``\gradle.properties`` with contents like: ``` -vcProtobufInclude=C:\\path\\to\\protobuf-3.2.0\\src -vcProtobufLibs=C:\\path\\to\\protobuf-3.2.0\\vsprojects\\Release +vcProtobufInclude=C:\\path\\to\\protobuf\\src +vcProtobufLibs=C:\\path\\to\\protobuf\\vsprojects\\Release targetArch=x86_32 ``` @@ -105,3 +114,39 @@ use the one that has been built by your own, by adding this property to ``` protoc=/path/to/protoc ``` + +How to install Android SDK +--------------------------- +This section is only necessary if you are building modules depending on Android +(e.g., `cronet`). Non-Android users only need to use `skipAndroid=true` as +discussed above. + +### Install via Android Studio (GUI) +Download and install Android Studio from [Android Developer site](https://developer.android.com/studio). +You can find the configuration for Android SDK at: +``` +Preferences -> System Settings -> Android SDK +``` +Select the version of Android SDK to be installed and click `apply`. The location +of Android SDK being installed is shown at `Android SDK Location` at the same panel. +The default is `$HOME/Library/Android/sdk` for Mac OS and `$HOME/Android/Sdk` for Linux. +You can change this to a custom location. + +### Install via Command line tools only +Go to [Android SDK](https://developer.android.com/studio#command-tools) and +download the commandlinetools package for your build machine OS. Decide where +you want the Android SDK to be stored. `$HOME/Library/Android/sdk` is typical on +Mac OS and `$HOME/Android/Sdk` for Linux. + +```sh +export ANDROID_HOME=$HOME/Android/Sdk # Adjust to your liking +mkdir $HOME/Android +mkdir $ANDROID_HOME +mkdir $ANDROID_HOME/cmdline-tools +unzip -d $ANDROID_HOME/cmdline-tools DOWNLOADS/commandlinetools-*.zip +mv $ANDROID_HOME/cmdline-tools/cmdline-tools $ANDROID_HOME/cmdline-tools/latest +# Android SDK is now ready. Now accept licenses so the build can auto-download packages +$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses + +# Add 'export ANDROID_HOME=$HOME/Android/Sdk' to your .bashrc or equivalent +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b9fdfef9de..646a7d986fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,27 @@ -# How to submit a bug report - -If you received an error message, please include it and any exceptions. +# How to contribute -We commonly need to know what platform you are on: - * JDK/JRE version (i.e., ```java -version```) - * Operating system (i.e., ```uname -a```) +We definitely welcome your patches and contributions to gRPC! Please read the gRPC +organization's [governance rules](https://github.com/grpc/grpc-community/blob/master/governance.md) +and [contribution guidelines](https://github.com/grpc/grpc-community/blob/master/CONTRIBUTING.md) before proceeding. -# How to contribute -We definitely welcome patches and contributions to grpc! Here are some -guideline and information about how to do so. +If you are new to github, please start by reading [Pull Request howto](https://help.github.com/articles/about-pull-requests/) -## Before getting started +## Legal requirements In order to protect both you and ourselves, you will need to sign the -[Contributor License Agreement](https://cla.developers.google.com/clas). +[Contributor License Agreement](https://easycla.lfx.linuxfoundation.org/). When +you make a PR, a CLA bot will provide a link for the process. + +## Compiling + +See [COMPILING.md](COMPILING.md). Specifically, you'll generally want to set +`skipCodegen=true` so you don't need to deal with the C++ compilation. + +## Code style We follow the [Google Java Style -Guide](https://google-styleguide.googlecode.com/svn/trunk/javaguide.html). Our +Guide](https://google.github.io/styleguide/javaguide.html). Our build automatically will provide warnings for style issues. [Eclipse](https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml) and @@ -26,15 +30,38 @@ style configurations are commonly useful. For IntelliJ 14, copy the style to `~/.IdeaIC14/config/codestyles/`, start IntelliJ, go to File > Settings > Code Style, and set the Scheme to `GoogleStyle`. -If planning on making a large change, feel free to [create an issue on -GitHub](https://github.com/grpc/grpc-java/issues/new), visit the [#grpc IRC -channel on Freenode](http://webchat.freenode.net/?channels=grpc), or send an -email to [grpc-io@googlegroups.com](grpc-io@googlegroups.com) to discuss -beforehand. +## Guidelines for Pull Requests +How to get your contributions merged smoothly and quickly. + +- Create **small PRs** that are narrowly focused on **addressing a single concern**. We often times receive PRs that are trying to fix several things at a time, but only one fix is considered acceptable, nothing gets merged and both author's & review's time is wasted. Create more PRs to address different concerns and everyone will be happy. + +- For speculative changes, consider opening an issue and discussing it to avoid + wasting time on an inappropriate approach. If you are suggesting a behavioral + or API change, consider starting with a [gRFC + proposal](https://github.com/grpc/proposal). + +- Follow [typical Git commit message](https://cbea.ms/git-commit/#seven-rules) + structure. Have a good **commit description** as a record of **what** and + **why** the change is being made. Link to a GitHub issue if it exists. The + commit description makes a good PR description and is auto-copied by GitHub if + you have a single commit when creating the PR. + + If your change is mostly for a single module (e.g., other module changes are + trivial), prefix your commit summary with the module name changed. Instead of + "Add HTTP/2 faster-than-light support to gRPC Netty" it is more terse as + "netty: Add faster-than-light support". + +- Don't fix code style and formatting unless you are already changing that line + to address an issue. If you do want to fix formatting or style, do that in a + separate PR. + +- Unless your PR is trivial, you should expect there will be reviewer comments + that you'll need to address before merging. Address comments with additional + commits so the reviewer can review just the changes; do not squash reviewed + commits unless the reviewer agrees. PRs are squashed when merging. -## Proposing changes +- Keep your PR up to date with upstream/master (if there are merge conflicts, we can't really merge your change). -Make sure that `./gradlew build` (`gradlew build` on Windows) completes -successfully without any new warnings. Then create a Pull Request with your -changes. When the changes are accepted, they will be merged or cherry-picked by -a gRPC core developer. +- **All tests need to be passing** before your change can be merged. We recommend you **run tests locally** before creating your PR to catch breakages early on. Also, `./gradlew build` (`gradlew build` on Windows) **must not introduce any new warnings**. + +- Exceptions to the rules can be made if there's a compelling reason for doing so. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 00000000000..d6ff2674710 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1 @@ +This repository is governed by the gRPC organization's [governance rules](https://github.com/grpc/grpc-community/blob/master/governance.md). diff --git a/LICENSE b/LICENSE index 2acb3d7503f..d6456956733 100644 --- a/LICENSE +++ b/LICENSE @@ -1,28 +1,202 @@ -Copyright 2014, Google Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 00000000000..5048c7c5aca --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,35 @@ +This page lists all active maintainers of this repository. If you were a +maintainer and would like to add your name to the Emeritus list, please send us a +PR. + +See [GOVERNANCE.md](https://github.com/grpc/grpc-community/blob/master/governance.md) +for governance guidelines and how to become a maintainer. +See [CONTRIBUTING.md](https://github.com/grpc/grpc-community/blob/master/CONTRIBUTING.md) +for general contribution guidelines. + +## Maintainers (in alphabetical order) +- [ejona86](https://github.com/ejona86), Google LLC +- [jdcormie](https://github.com/jdcormie), Google LLC +- [kannanjgithub](https://github.com/kannanjgithub), Google LLC +- [ran-su](https://github.com/ran-su), Google LLC +- [sergiitk](https://github.com/sergiitk), Google LLC +- [temawi](https://github.com/temawi), Google LLC +- [YifeiZhuang](https://github.com/YifeiZhuang), Google LLC +- [zhangkun83](https://github.com/zhangkun83), Google LLC + +## Emeritus Maintainers (in alphabetical order) +- [carl-mastrangelo](https://github.com/carl-mastrangelo) +- [creamsoup](https://github.com/creamsoup) +- [dapengzhang0](https://github.com/dapengzhang0) +- [ericgribkoff](https://github.com/ericgribkoff) +- [jiangtaoli2016](https://github.com/jiangtaoli2016) +- [jtattermusch](https://github.com/jtattermusch) +- [larry-safran](https://github.com/larry-safran) +- [louiscryan](https://github.com/louiscryan) +- [markb74](https://github.com/markb74) +- [nicolasnoble](https://github.com/nicolasnoble) +- [nmittler](https://github.com/nmittler) +- [sanjaypujare](https://github.com/sanjaypujare) +- [srini100](https://github.com/srini100) +- [voidzcy](https://github.com/voidzcy) +- [zpencer](https://github.com/zpencer) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 00000000000..81d80ed7e1a --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,159 @@ +module( + name = "grpc-java", + version = "1.80.0-SNAPSHOT", # CURRENT_GRPC_VERSION + compatibility_level = 0, + repo_name = "io_grpc_grpc_java", +) + +# GRPC_DEPS_START +IO_GRPC_GRPC_JAVA_ARTIFACTS = [ + "com.google.android:annotations:4.1.1.4", + "com.google.api.grpc:proto-google-common-protos:2.63.2", + "com.google.auth:google-auth-library-credentials:1.41.0", + "com.google.auth:google-auth-library-oauth2-http:1.41.0", + "com.google.auto.value:auto-value-annotations:1.11.0", + "com.google.auto.value:auto-value:1.11.0", + "com.google.code.findbugs:jsr305:3.0.2", + "com.google.code.gson:gson:2.12.1", + "com.google.errorprone:error_prone_annotations:2.45.0", + "com.google.guava:failureaccess:1.0.1", + "com.google.guava:guava:33.5.0-android", + "com.google.re2j:re2j:1.8", + "com.google.s2a.proto.v2:s2a-proto:0.1.3", + "com.google.truth:truth:1.4.5", + "com.squareup.okhttp:okhttp:2.7.5", + "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day + "io.netty:netty-buffer:4.1.130.Final", + "io.netty:netty-codec-http2:4.1.130.Final", + "io.netty:netty-codec-http:4.1.130.Final", + "io.netty:netty-codec-socks:4.1.130.Final", + "io.netty:netty-codec:4.1.130.Final", + "io.netty:netty-common:4.1.130.Final", + "io.netty:netty-handler-proxy:4.1.130.Final", + "io.netty:netty-handler:4.1.130.Final", + "io.netty:netty-resolver:4.1.130.Final", + "io.netty:netty-tcnative-boringssl-static:2.0.74.Final", + "io.netty:netty-tcnative-classes:2.0.74.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.130.Final", + "io.netty:netty-transport-native-unix-common:4.1.130.Final", + "io.netty:netty-transport:4.1.130.Final", + "io.opencensus:opencensus-api:0.31.0", + "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", + "io.perfmark:perfmark-api:0.27.0", + "junit:junit:4.13.2", + "org.checkerframework:checker-qual:3.49.5", + "org.codehaus.mojo:animal-sniffer-annotations:1.26", +] +# GRPC_DEPS_END + +bazel_dep(name = "bazel_jar_jar", version = "0.1.11.bcr.1") +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "googleapis", version = "0.0.0-20240326-1c8d509c5", repo_name = "com_google_googleapis") +bazel_dep(name = "grpc-proto", version = "0.0.0-20240627-ec30f58.bcr.1", repo_name = "io_grpc_grpc_proto") +bazel_dep(name = "protobuf", version = "33.4", repo_name = "com_google_protobuf") +bazel_dep(name = "rules_cc", version = "0.0.9") +bazel_dep(name = "rules_java", version = "9.1.0") +bazel_dep(name = "rules_jvm_external", version = "6.0") + +maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") +maven.install( + artifacts = IO_GRPC_GRPC_JAVA_ARTIFACTS, + repositories = [ + "https://repo.maven.apache.org/maven2/", + ], + strict_visibility = True, +) +use_repo(maven, "maven") + +maven.override( + coordinates = "com.google.protobuf:protobuf-java", + target = "@com_google_protobuf//:protobuf_java", +) +maven.override( + coordinates = "com.google.protobuf:protobuf-java-util", + target = "@com_google_protobuf//:protobuf_java_util", +) +maven.override( + coordinates = "com.google.protobuf:protobuf-javalite", + target = "@com_google_protobuf//:protobuf_javalite", +) +maven.override( + coordinates = "io.grpc:grpc-alts", + target = "@io_grpc_grpc_java//alts", +) +maven.override( + coordinates = "io.grpc:grpc-api", + target = "@io_grpc_grpc_java//api", +) +maven.override( + coordinates = "io.grpc:grpc-auth", + target = "@io_grpc_grpc_java//auth", +) +maven.override( + coordinates = "io.grpc:grpc-census", + target = "@io_grpc_grpc_java//census", +) +maven.override( + coordinates = "io.grpc:grpc-context", + target = "@io_grpc_grpc_java//context", +) +maven.override( + coordinates = "io.grpc:grpc-core", + target = "@io_grpc_grpc_java//core:core_maven", +) +maven.override( + coordinates = "io.grpc:grpc-googleapis", + target = "@io_grpc_grpc_java//googleapis", +) +maven.override( + coordinates = "io.grpc:grpc-grpclb", + target = "@io_grpc_grpc_java//grpclb", +) +maven.override( + coordinates = "io.grpc:grpc-inprocess", + target = "@io_grpc_grpc_java//inprocess", +) +maven.override( + coordinates = "io.grpc:grpc-netty", + target = "@io_grpc_grpc_java//netty", +) +maven.override( + coordinates = "io.grpc:grpc-netty-shaded", + target = "@io_grpc_grpc_java//netty:shaded_maven", +) +maven.override( + coordinates = "io.grpc:grpc-okhttp", + target = "@io_grpc_grpc_java//okhttp", +) +maven.override( + coordinates = "io.grpc:grpc-protobuf", + target = "@io_grpc_grpc_java//protobuf", +) +maven.override( + coordinates = "io.grpc:grpc-protobuf-lite", + target = "@io_grpc_grpc_java//protobuf-lite", +) +maven.override( + coordinates = "io.grpc:grpc-rls", + target = "@io_grpc_grpc_java//rls", +) +maven.override( + coordinates = "io.grpc:grpc-services", + target = "@io_grpc_grpc_java//services:services_maven", +) +maven.override( + coordinates = "io.grpc:grpc-stub", + target = "@io_grpc_grpc_java//stub", +) +maven.override( + coordinates = "io.grpc:grpc-testing", + target = "@io_grpc_grpc_java//testing", +) +maven.override( + coordinates = "io.grpc:grpc-xds", + target = "@io_grpc_grpc_java//xds:xds_maven", +) +maven.override( + coordinates = "io.grpc:grpc-util", + target = "@io_grpc_grpc_java//util", +) diff --git a/NOTICE.txt b/NOTICE.txt index ee67bad4ba4..f70c5620cf7 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,31 +1,16 @@ -Copyright 2014, Google Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright 2014 The gRPC Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ----------------------------------------------------------------------- @@ -38,4 +23,40 @@ at: * HOMEPAGE: * https://github.com/square/okhttp * LOCATION_IN_GRPC: - * okhttp/third_party/okhttp \ No newline at end of file + * okhttp/third_party/okhttp + +This product contains a modified portion of 'Envoy', an open source +cloud-native high-performance edge/middle/service proxy, which can be +obtained at: + + * LICENSE: + * xds/third_party/envoy/LICENSE (Apache License 2.0) + * NOTICE: + * xds/third_party/envoy/NOTICE + * HOMEPAGE: + * https://www.envoyproxy.io + * LOCATION_IN_GRPC: + * xds/third_party/envoy + +This product contains a modified portion of 'protoc-gen-validate (PGV)', +an open source protoc plugin to generate polyglot message validators, +which can be obtained at: + + * LICENSE: + * xds/third_party/protoc-gen-validate/LICENSE (Apache License 2.0) + * NOTICE: + * xds/third_party/protoc-gen-validate/NOTICE + * HOMEPAGE: + * https://github.com/envoyproxy/protoc-gen-validate + * LOCATION_IN_GRPC: + * xds/third_party/protoc-gen-validate + +This product contains a modified portion of 'udpa', +an open source universal data plane API, which can be obtained at: + + * LICENSE: + * xds/third_party/udpa/LICENSE (Apache License 2.0) + * HOMEPAGE: + * https://github.com/cncf/udpa + * LOCATION_IN_GRPC: + * xds/third_party/udpa diff --git a/PATENTS b/PATENTS deleted file mode 100644 index 619f9dbfe63..00000000000 --- a/PATENTS +++ /dev/null @@ -1,22 +0,0 @@ -Additional IP Rights Grant (Patents) - -"This implementation" means the copyrightable works distributed by -Google as part of the GRPC project. - -Google hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) -patent license to make, have made, use, offer to sell, sell, import, -transfer and otherwise run, modify and propagate the contents of this -implementation of GRPC, where such license applies only to those patent -claims, both currently owned or controlled by Google and acquired in -the future, licensable by Google that are necessarily infringed by this -implementation of GRPC. This grant does not include claims that would be -infringed only as a consequence of further modification of this -implementation. If you or your agent or exclusive licensee institute or -order or agree to the institution of patent litigation against any -entity (including a cross-claim or counterclaim in a lawsuit) alleging -that this implementation of GRPC or any code incorporated within this -implementation of GRPC constitutes direct or contributory patent -infringement, or inducement of patent infringement, then any patent -rights granted to you under this License for this implementation of GRPC -shall terminate as of the date such litigation is filed. diff --git a/README.md b/README.md index 2aab764bb0a..205187b4000 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,10 @@ gRPC-Java - An RPC library and framework ======================================== -gRPC-Java works with JDK 6. TLS usage typically requires using Java 8, or Play -Services Dynamic Security Provider on Android. Please see the [Security -Readme](SECURITY.md). - - + @@ -17,8 +13,40 @@ Readme](SECURITY.md).
Homepage:www.grpc.iogrpc.io
Mailing List:
[![Join the chat at https://gitter.im/grpc/grpc](https://badges.gitter.im/grpc/grpc.svg)](https://gitter.im/grpc/grpc?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://travis-ci.org/grpc/grpc-java.svg?branch=master)](https://travis-ci.org/grpc/grpc-java) -[![Coverage Status](https://coveralls.io/repos/grpc/grpc-java/badge.svg?branch=master&service=github)](https://coveralls.io/github/grpc/grpc-java?branch=master) +[![GitHub Actions Linux Testing](https://github.com/grpc/grpc-java/actions/workflows/testing.yml/badge.svg?branch=master)](https://github.com/grpc/grpc-java/actions/workflows/testing.yml?branch=master) +[![Line Coverage Status](https://coveralls.io/repos/grpc/grpc-java/badge.svg?branch=master&service=github)](https://coveralls.io/github/grpc/grpc-java?branch=master) +[![Branch-adjusted Line Coverage Status](https://codecov.io/gh/grpc/grpc-java/branch/master/graph/badge.svg)](https://codecov.io/gh/grpc/grpc-java) + +Supported Platforms +------------------- + +gRPC-Java supports Java 8 and later. Android minSdkVersion 21 (Lollipop) and +later are supported with [Java 8 language desugaring][android-java-8]. + +TLS usage on Android typically requires Play Services Dynamic Security Provider. +Please see the [Security Readme](SECURITY.md). + +Older Java versions are not directly supported, but a branch remains available +for fixes and releases. See [gRFC P5 JDK Version Support +Policy][P5-jdk-version-support]. + +Java version | gRPC Branch +------------ | ----------- +7 | 1.41.x + +[android-java-8]: https://developer.android.com/studio/write/java8-support#supported_features +[P5-jdk-version-support]: https://github.com/grpc/proposal/blob/master/P5-jdk-version-support.md#proposal + +Getting Started +--------------- + +For a guided tour, take a look at the [quick start +guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC +basics](https://grpc.io/docs/languages/java/basics). + +The [examples](https://github.com/grpc/grpc-java/tree/v1.78.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.78.0/examples/android) +are standalone projects that showcase the usage of gRPC. Download -------- @@ -27,41 +55,49 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: ```xml io.grpc - grpc-netty - 1.0.3 + grpc-netty-shaded + 1.78.0 + runtime io.grpc grpc-protobuf - 1.0.3 + 1.78.0 io.grpc grpc-stub - 1.0.3 + 1.78.0 ``` Or for Gradle with non-Android, add to your dependencies: ```gradle -compile 'io.grpc:grpc-netty:1.0.3' -compile 'io.grpc:grpc-protobuf:1.0.3' -compile 'io.grpc:grpc-stub:1.0.3' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.78.0' +implementation 'io.grpc:grpc-protobuf:1.78.0' +implementation 'io.grpc:grpc-stub:1.78.0' ``` -For Android client, use `grpc-okhttp` instead of `grpc-netty` and -`grpc-protobuf-lite` or `grpc-protobuf-nano` instead of `grpc-protobuf`: +For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and +`grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -compile 'io.grpc:grpc-okhttp:1.0.3' -compile 'io.grpc:grpc-protobuf-lite:1.0.3' -compile 'io.grpc:grpc-stub:1.0.3' +implementation 'io.grpc:grpc-okhttp:1.78.0' +implementation 'io.grpc:grpc-protobuf-lite:1.78.0' +implementation 'io.grpc:grpc-stub:1.78.0' ``` +For [Bazel](https://bazel.build), you can either +[use Maven](https://github.com/bazelbuild/rules_jvm_external) +(with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). + [the JARs]: -http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22io.grpc%22%20AND%20v%3A%221.0.3%22 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.78.0 Development snapshots are available in [Sonatypes's snapshot -repository](https://oss.sonatype.org/content/repositories/snapshots/). +repository](https://central.sonatype.com/repository/maven-snapshots/). + +Generated Code +-------------- For protobuf-based codegen, you can put your proto files in the `src/main/proto` and `src/test/proto` directories along with an appropriate plugin. @@ -76,18 +112,18 @@ For protobuf-based codegen integrated with the Maven build system, you can use kr.motd.maven os-maven-plugin - 1.4.1.Final + 1.7.1 org.xolstice.maven.plugins protobuf-maven-plugin - 0.5.0 + 0.6.1 - com.google.protobuf:protoc:3.0.2:exe:${os.detected.classifier} + com.google.protobuf:protoc:3.25.8:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.0.3:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.78.0:exe:${os.detected.classifier} @@ -104,30 +140,20 @@ For protobuf-based codegen integrated with the Maven build system, you can use [protobuf-maven-plugin]: https://www.xolstice.org/protobuf-maven-plugin/ -For protobuf-based codegen integrated with the Gradle build system, you can use -[protobuf-gradle-plugin][]: +For non-Android protobuf-based codegen integrated with the Gradle build system, +you can use [protobuf-gradle-plugin][]: ```gradle -apply plugin: 'java' -apply plugin: 'com.google.protobuf' - -buildscript { - repositories { - mavenCentral() - } - dependencies { - // ASSUMES GRADLE 2.12 OR HIGHER. Use plugin version 0.7.5 with earlier - // gradle versions - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0' - } +plugins { + id 'com.google.protobuf' version '0.9.5' } protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.0.2" + artifact = "com.google.protobuf:protoc:3.25.8" } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.0.3' + artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0' } } generateProtoTasks { @@ -140,18 +166,74 @@ protobuf { [protobuf-gradle-plugin]: https://github.com/google/protobuf-gradle-plugin +The prebuilt protoc-gen-grpc-java binary uses glibc on Linux. If you are +compiling on Alpine Linux, you may want to use the [Alpine grpc-java package][] +which uses musl instead. + +[Alpine grpc-java package]: https://pkgs.alpinelinux.org/package/edge/community/x86_64/grpc-java + +For Android protobuf-based codegen integrated with the Gradle build system, also +use protobuf-gradle-plugin but specify the 'lite' options: + +```gradle +plugins { + id 'com.google.protobuf' version '0.9.5' +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.25.8" + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0' + } + } + generateProtoTasks { + all().each { task -> + task.builtins { + java { option 'lite' } + } + task.plugins { + grpc { option 'lite' } + } + } + } +} + +``` + +For [Bazel](https://bazel.build), use the [`proto_library`](https://github.com/bazelbuild/rules_proto) +and the [`java_proto_library`](https://bazel.build/reference/be/java#java_proto_library) (no `load()` required) +and `load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library")` (from this project), as in +[this example `BUILD.bazel`](https://github.com/grpc/grpc-java/blob/master/examples/BUILD.bazel). + +API Stability +------------- + +APIs annotated with `@Internal` are for internal use by the gRPC library and +should not be used by gRPC users. APIs annotated with `@ExperimentalApi` are +subject to change in future releases, and library code that other projects +may depend on should not use these APIs. + +We recommend using the +[grpc-java-api-checker](https://github.com/grpc/grpc-java-api-checker) +(an [Error Prone](https://github.com/google/error-prone) plugin) +to check for usages of `@ExperimentalApi` and `@Internal` in any library code +that depends on gRPC. It may also be used to check for `@Internal` usage or +unintended `@ExperimentalApi` consumption in non-library code. + How to Build ------------ If you are making changes to gRPC-Java, see the [compiling instructions](COMPILING.md). -Navigating Around the Source ----------------------------- +High-level Components +--------------------- -Here's a quick readers' guide to the code to help folks get started. At a high -level there are three distinct layers to the library: __Stub__, __Channel__ & -__Transport__. +At a high level there are three distinct layers to the library: *Stub*, +*Channel*, and *Transport*. ### Stub @@ -159,81 +241,34 @@ The Stub layer is what is exposed to most developers and provides type-safe bindings to whatever datamodel/IDL/interface you are adapting. gRPC comes with a [plugin](https://github.com/google/grpc-java/blob/master/compiler) to the protocol-buffers compiler that generates Stub interfaces out of `.proto` files, -but bindings to other datamodel/IDL should be trivial to add and are welcome. - -#### Key Interfaces - -[Stream Observer](https://github.com/google/grpc-java/blob/master/stub/src/main/java/io/grpc/stub/StreamObserver.java) +but bindings to other datamodel/IDL are easy and encouraged. ### Channel The Channel layer is an abstraction over Transport handling that is suitable for interception/decoration and exposes more behavior to the application than the Stub layer. It is intended to be easy for application frameworks to use this -layer to address cross-cutting concerns such as logging, monitoring, auth etc. -Flow-control is also exposed at this layer to allow more sophisticated -applications to interact with it directly. - -#### Common - -* [Metadata - headers & trailers](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/Metadata.java) -* [Status - error code namespace & handling](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/Status.java) - -#### Client -* [Channel - client side binding](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/Channel.java) -* [Client Call](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/ClientCall.java) -* [Client Interceptor](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/ClientInterceptor.java) - -#### Server -* [Server call handler - analog to Channel on server](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/ServerCallHandler.java) -* [Server Call](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/ServerCall.java) - +layer to address cross-cutting concerns such as logging, monitoring, auth, etc. ### Transport The Transport layer does the heavy lifting of putting and taking bytes off the wire. The interfaces to it are abstract just enough to allow plugging in of -different implementations. Transports are modeled as `Stream` factories. The -variation in interface between a server Stream and a client Stream exists to -codify their differing semantics for cancellation and error reporting. - -Note the transport layer API is considered internal to gRPC and has weaker API -guarantees than the core API under package `io.grpc`. - -gRPC comes with three Transport implementations: - -1. The [Netty-based](https://github.com/google/grpc-java/blob/master/netty) - transport is the main transport implementation based on - [Netty](http://netty.io). It is for both the client and the server. -2. The [OkHttp-based](https://github.com/google/grpc-java/blob/master/okhttp) - transport is a lightweight transport based on - [OkHttp](http://square.github.io/okhttp/). It is mainly for use on Android - and is for client only. -3. The - [inProcess](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/inprocess) - transport is for when a server is in the same process as the client. It is - useful for testing. - -#### Common - -* [Stream](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/internal/Stream.java) -* [Stream Listener](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/internal/StreamListener.java) - -#### Client - -* [Client Stream](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/internal/ClientStream.java) -* [Client Stream Listener](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/internal/ClientStreamListener.java) - -#### Server - -* [Server Stream](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/internal/ServerStream.java) -* [Server Stream Listener](https://github.com/google/grpc-java/blob/master/core/src/main/java/io/grpc/internal/ServerStreamListener.java) - - -Examples --------- - -The [examples](https://github.com/grpc/grpc-java/tree/master/examples) -and the -[Android example](https://github.com/grpc/grpc-java/tree/master/examples/android) are standalone projects that -showcase the usage of gRPC. +different implementations. Note the transport layer API is considered internal +to gRPC and has weaker API guarantees than the core API under package `io.grpc`. + +gRPC comes with multiple Transport implementations: + +1. The Netty-based HTTP/2 transport is the main transport implementation based + on [Netty](https://netty.io). It is not officially supported on Android. + There is a "grpc-netty-shaded" version of this transport. It is generally + preferred over using the Netty-based transport directly as it requires less + dependency management and is easier to upgrade within many applications. +2. The OkHttp-based HTTP/2 transport is a lightweight transport based on + [Okio](https://square.github.io/okio/) and forked low-level parts of + [OkHttp](https://square.github.io/okhttp/). It is mainly for use on Android. +3. The in-process transport is for when a server is in the same process as the + client. It is used frequently for testing, while also being safe for + production use. +4. The Binder transport is for Android cross-process communication on a single + device. diff --git a/RELEASING.md b/RELEASING.md index c7a63dc688e..c57829b8c25 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -4,384 +4,249 @@ How to Create a Release of GRPC Java (for Maintainers Only) Build Environments ------------------ We deploy GRPC to Maven Central under the following systems: -- Ubuntu 14.04 with Docker 1.6.1 that runs CentOS 6.6 -- Windows 7 64-bit with MSYS2 with mingw32 and mingw64 -- Mac OS X 10.9.5 +- Ubuntu 14.04 with Docker 13.03.0 that runs CentOS 7 +- Windows 7 64-bit with Visual Studio +- Mac OS X 10.14.6 Other systems may also work, but we haven't verified them. -Prerequisites -------------- - -### Setup OSSRH and Signing - -If you haven't deployed artifacts to Maven Central before, you need to setup -your OSSRH (OSS Repository Hosting) account and signing keys. -- Follow the instructions on [this - page](http://central.sonatype.org/pages/ossrh-guide.html) to set up an - account with OSSRH. - - You only need to create the account, not set up a new project - - Contact a gRPC maintainer to add your account after you have created it. -- (For release deployment only) Install GnuPG and [generate your key - pair](https://www.gnupg.org/documentation/howtos.html). You'll also - need to [publish your public key](https://www.gnupg.org/gph/en/manual.html#AEN464) - to make it visible to the Sonatype servers - (e.g. `gpg --keyserver pgp.mit.edu --send-key `). -- Put your GnuPG key password and OSSRH account information in - `/.gradle/gradle.properties`. +Common Variables +---------------- +Many of the following commands expect release-specific variables to be set. Set +them before continuing, and set them again when resuming. +```bash +MAJOR=1 MINOR=7 PATCH=0 # Set appropriately for new release +VERSION_FILES=( + MODULE.bazel + build.gradle + core/src/main/java/io/grpc/internal/GrpcUtil.java + examples/MODULE.bazel + examples/build.gradle + examples/pom.xml + examples/android/clientcache/app/build.gradle + examples/android/helloworld/app/build.gradle + examples/android/routeguide/app/build.gradle + examples/android/strictmode/app/build.gradle + examples/example-*/build.gradle + examples/example-*/pom.xml + ) ``` -# You need the signing properties only if you are making release deployment -signing.keyId=<8-character-public-key-id> -signing.password= -signing.secretKeyRingFile=/.gnupg/secring.gpg - -ossrhUsername= -ossrhPassword= -checkstyle.ignoreFailures=false -``` - -### Build Protobuf -Protobuf libraries are needed for compiling the GRPC codegen. Despite that you -may have installed Protobuf on your system, you may want to build Protobuf -separately and install it under your personal directory, because -1. The Protobuf version installed on your system may be different from what - GRPC requires. You may not want to pollute your system installation. -2. We will deploy both 32-bit and 64-bit versions of the codegen, thus require - both variants of Protobuf libraries. You don't want to mix them in your - system paths. -Please see the [Main Readme](README.md) for details on building protobuf. - -Tagging the Release ----------------------- -The first step in the release process is to create a release branch, bump -versions, and create a tag for the release. Our release branches follow the naming +Branching the Release +--------------------- +The first step in the release process is to create a release branch and bump +the SNAPSHOT version. Our release branches follow the naming convention of `v..x`, while the tags include the patch version -`v..`. For example, the same branch `v0.7.x` -would be used to create all `v0.7` tags (e.g. `v0.7.0`, `v0.7.1`). +`v..`. For example, the same branch `v1.7.x` +would be used to create all `v1.7` tags (e.g. `v1.7.0`, `v1.7.1`). -1. Create the release branch and push it to GitHub: - - ```bash - $ MAJOR=0 MINOR=7 PATCH=0 # Set appropriately for new release - $ VERSION_FILES=( - build.gradle - android-interop-testing/app/build.gradle - examples/build.gradle - examples/pom.xml - examples/android/helloworld/app/build.gradle - examples/android/routeguide/app/build.gradle - examples/thrift/build.gradle - ) - $ git checkout -b v$MAJOR.$MINOR.x master - $ git push upstream v$MAJOR.$MINOR.x - ``` +1. Review the issues in the current release [milestone](https://github.com/grpc/grpc-java/milestones) + for issues that won't make the cut. Check if any of them can be + closed. Be aware of the issues with the [TODO:release blocker][] label. + Consider reaching out to the assignee for the status update. 2. For `master`, change root build files to the next minor snapshot (e.g. - ``0.8.0-SNAPSHOT``). + ``1.8.0-SNAPSHOT``). ```bash - $ git checkout -b bump-version master + git checkout -b bump-version master # Change version to next minor (and keep -SNAPSHOT) - $ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_GRPC_VERSION\)/'$MAJOR.$((MINOR+1)).0'\1/' \ + sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_GRPC_VERSION\)/'$MAJOR.$((MINOR+1)).0'\1/' \ "${VERSION_FILES[@]}" - $ ./gradlew build - $ git commit -a -m "Start $MAJOR.$((MINOR+1)).0 development cycle" - ``` -3. Go through PR review and push the master branch to GitHub: - - ```bash - $ git checkout master - $ git merge --ff-only bump-version - $ git push upstream master + sed -i s/$MAJOR.$MINOR.$PATCH/$MAJOR.$((MINOR+1)).0/ \ + compiler/src/test{,Lite}/golden/Test{,Deprecated}Service.java.txt + ./gradlew build + git commit -a -m "Start $MAJOR.$((MINOR+1)).0 development cycle" ``` -4. For vMajor.Minor.x branch, change `README.md` to refer to the next release - version. _Also_ update the version numbers for protoc if the protobuf library - version was updated since the last release. +3. Go through PR review and submit. +4. Create the release branch starting just before your commit and push it to GitHub: ```bash - $ git checkout -b release v$MAJOR.$MINOR.x - # Bump documented versions. Don't forget protobuf version - $ ${EDITOR:-nano -w} README.md - $ git commit -a -m "Update README to reference $MAJOR.$MINOR.$PATCH" + git fetch upstream + git checkout -b v$MAJOR.$MINOR.x \ + $(git log --pretty=format:%H --grep "^Start $MAJOR.$((MINOR+1)).0 development cycle" upstream/master)^ + git push upstream v$MAJOR.$MINOR.x ``` -5. Change root build files to remove "-SNAPSHOT" for the next release version - (e.g. `0.7.0`). Commit the result and make a tag: +5. Continue with Google-internal steps at go/grpc-java/releasing, but stop + before `Auto releasing using kokoro`. +6. Create a milestone for the next release. +7. Move items out of the release milestone that didn't make the cut. Issues that + may be backported should stay in the release milestone. Treat issues with the + 'release blocker' label with special care. +8. Begin compiling release notes. This produces a starting point: ```bash - # Change version to remove -SNAPSHOT - $ sed -i 's/-SNAPSHOT\(.*CURRENT_GRPC_VERSION\)/\1/' "${VERSION_FILES[@]}" - $ ./gradlew build - $ git commit -a -m "Bump version to $MAJOR.$MINOR.$PATCH" - $ git tag -a v$MAJOR.$MINOR.$PATCH -m "Version $MAJOR.$MINOR.$PATCH" + echo "## gRPC Java $MAJOR.$MINOR.0 Release Notes" && echo && \ + git shortlog -e --format='%s (%h)' "$(git merge-base upstream/v$MAJOR.$((MINOR-1)).x upstream/v$MAJOR.$MINOR.x)"..upstream/v$MAJOR.$MINOR.x | cat && \ + echo && echo && echo "Backported commits in previous release:" && \ + git log --oneline "$(git merge-base v$MAJOR.$((MINOR-1)).0 upstream/v$MAJOR.$MINOR.x)"..v$MAJOR.$((MINOR-1)).0^ ``` -6. Change root build files to the next snapshot version (e.g. `0.7.1-SNAPSHOT`). - Commit the result: - ```bash - # Change version to next patch and add -SNAPSHOT - $ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_GRPC_VERSION\)/'$MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT'\1/' \ - "${VERSION_FILES[@]}" - $ ./gradlew build - $ git commit -a -m "Bump version to $MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT" - ``` -7. Go through PR review and push the release tag and updated release branch to - GitHub: +[TODO:release blocker]: https://github.com/grpc/grpc-java/issues?q=label%3A%22TODO%3Arelease+blocker%22 +[TODO:backport]: https://github.com/grpc/grpc-java/issues?q=label%3ATODO%3Abackport - ```bash - $ git checkout v$MAJOR.$MINOR.x - $ git merge --ff-only release - $ git push upstream v$MAJOR.$MINOR.$PATCH - $ git push upstream v$MAJOR.$MINOR.x - ``` -8. Make sure you are [logged in](https://grpc-testing.appspot.com/manage) to - Jenkins, then make a [new release - job](https://grpc-testing.appspot.com/view/Releases/newJob) - * _Name_: gRPC-Java-$MAJOR.$MINOR-Windows - * _Copy from_: gRPC-Java-master-windows - * Click _OK_ button - * _Display Name_ under _Use custom workspace_ (not ~~Project - url~~): gRPC Java $MAJOR.$MINOR Windows - * Under _Source Code Management_, _Branches to build_'s - _Branch Specifier_: `*/v$MAJOR.$MINOR.x` - * Click _SAVE_ button - * Click _Build Now_ - * Click on job #1, then _Console Output_. Verify the `git checkout` checked - out the correct commit - -Setup Build Environment ---------------------------- - -### Linux -The deployment for Linux uses [Docker](https://www.docker.com/) running -CentOS 6.6 in order to ensure that we have a consistent deployment environment -on Linux. You'll first need to install Docker if not already installed on your -system. Make sure to have at least version 1.7.1 or later. - -1. Under the [Protobuf source directory](https://github.com/google/protobuf), - build the `protoc-artifacts` image: - - ```bash - protobuf$ docker build -t protoc-artifacts protoc-artifacts - ``` -2. Under the grpc-java source directory, build the `grpc-java-deploy` image: +Tagging the Release +------------------- + +1. Verify there are no open issues in the release milestone. Open issues should + either be deferred or resolved and the fix backported. Verify there are no + [TODO:release blocker][] nor [TODO:backport][] issues (open or closed), or + that they are tracking an issue for a different branch. +2. Ensure that the Google-internal steps + at go/grpc-java/releasing#before-tagging-a-release are completed. +3. For vMajor.Minor.x branch, change `README.md` to refer to the next release + version. _Also_ update the version numbers for protoc if the protobuf library + version was updated since the last release. ```bash - grpc-java$ docker build -t grpc-java-deploy compiler - ``` -3. Start a Docker container that has the deploy environment set up for you. The - GRPC source is cloned into `/grpc-java`. + git checkout v$MAJOR.$MINOR.x + git pull upstream v$MAJOR.$MINOR.x + git checkout -b release-v$MAJOR.$MINOR.$PATCH - ```bash - $ docker run -it --rm=true grpc-java-deploy - ``` + # Bump documented gRPC versions. + # Also update protoc version to match protobuf version in gradle/libs.versions.toml. + ${EDITOR:-nano -w} README.md - Note that the container will be deleted after you exit. Any changes you have - made (e.g., copied configuration files) will be lost. If you want to keep the - container, remove `--rm=true` from the command line. -4. Next, you'll need to copy your OSSRH credentials and GnuPG keys to your docker container. - In Docker: + git commit -a -m "Update README etc to reference $MAJOR.$MINOR.$PATCH" ``` - # mkdir /root/.gradle - ``` - Find the container ID in your bash prompt, which is shown as `[root@ ...]`. - In host: - ``` - $ docker cp ~/.gnupg :/root/ - $ docker cp ~/.gradle/gradle.properties :/root/.gradle/ - ``` - - You'll also need to update `signing.secretKeyRingFile` in - `/root/.gradle/gradle.properties` to point to `/root/.gnupg/secring.gpg`. - -### Windows - -#### Windows 64-bit with MSYS2 (Recommended for Windows) -Because the gcc shipped with MSYS2 doesn't support multilib, you have to -compile and deploy 32-bit and 64-bit binaries in separate steps. - -##### Under MinGW-w64 Win32 Shell -1. Compile and install 32-bit protobuf: - - ```bash - protobuf$ ./configure --disable-shared --prefix=$HOME/protobuf-32 - protobuf$ make clean && make && make install - ``` -2. Configure CXXFLAGS needed by the protoc plugin when building. - - ```bash - grpc-java$ export CXXFLAGS="-I$HOME/protobuf-32/include" \ - LDFLAGS="-L$HOME/protobuf-32/lib" - ``` - -##### Under MinGW-w64 Win64 Shell -1. Compile and install 64-bit protobuf: - - ```bash - protobuf$ ./configure --disable-shared --prefix=$HOME/protobuf-64 - protobuf$ make clean && make && make install - ``` -2. Configure CXXFLAGS needed by the protoc plugin when building. - - ```bash - grpc-java$ export CXXFLAGS="-I$HOME/protobuf-64/include" \ - LDFLAGS="-L$HOME/protobuf-64/lib" - ``` - -#### Windows 64-bit with Cygwin64 (TODO: incomplete) -Because the MinGW gcc shipped with Cygwin64 doesn't support multilib, you have -to compile and deploy 32-bit and 64-bit binaries in separate steps. - -1. Compile and install 32-bit protobuf. `-static-libgcc -static-libstdc++` are - needed for `protoc` to be successfully run in the unit test. - - ```bash - protobuf$ LDFLAGS="-static-libgcc -static-libstdc++" ./configure --host=i686-w64-mingw32 --disable-shared --prefix=$HOME/protobuf-32 - protobuf$ make clean && make && make install - ``` - -2. Compile and install 64-bit protobuf: - - ```bash - protobuf$ ./configure --host=x86_64-w64-mingw32 --disable-shared --prefix=$HOME/protobuf-64 - protobuf$ make clean && make && make install - ``` - -### Mac -Please refer to [Protobuf -README](https://github.com/google/protobuf/blob/master/README.md) for how to -set up GCC and Unix tools on Mac. - -Mac OS X has been 64-bit-only since 10.7 and we are compiling for 10.7 and up. -We only build 64-bit artifact for Mac. - -1. Compile and install protobuf: +4. Change root build files to remove "-SNAPSHOT" for the next release version + (e.g. `0.7.0`). Commit the result and make a tag: ```bash - protobuf$ CXXFLAGS="-m64" ./configure --disable-shared --prefix=$HOME/protobuf - protobuf$ make clean && make && make install + # Change version to remove -SNAPSHOT + sed -i 's/-SNAPSHOT\(.*CURRENT_GRPC_VERSION\)/\1/' "${VERSION_FILES[@]}" + sed -i s/-SNAPSHOT// compiler/src/test{,Lite}/golden/Test{,Deprecated}Service.java.txt + ./gradlew build + git commit -a -m "Bump version to $MAJOR.$MINOR.$PATCH" + git tag -a v$MAJOR.$MINOR.$PATCH -m "Version $MAJOR.$MINOR.$PATCH" ``` -2. Configure CXXFLAGS needed by the protoc plugin when building. +5. Change root build files to the next snapshot version (e.g. `0.7.1-SNAPSHOT`). + Commit the result: ```bash - grpc-java$ export CXXFLAGS="-I$HOME/protobuf/include" \ - LDFLAGS="$HOME/protobuf/lib/libprotobuf.a $HOME/protobuf/lib/libprotoc.a" - ``` - -Build and Deploy ----------------- -We currently distribute the following OSes and architectures: - -| OS | x86_32 | x86_64 | -| --- | --- | --- | -| Linux | X | X | -| Windows | X | X | -| Mac | | X | - -Deployment to Maven Central (or the snapshot repo) is a two-step process. The only -artifact that is platform-specific is codegen, so we only need to deploy the other -jars once. So the first deployment is for all of the artifacts from one of the selected -OS/architectures. After that, we then deploy the codegen artifacts for the remaining -OS/architectures. - -**NOTE: _Before building/deploying, be sure to switch to the appropriate branch or tag in -the grpc-java source directory._** - -### First Deployment - -As stated above, this only needs to be done once for one of the selected OS/architectures. -The following command will build the whole project and upload it to Maven -Central. Parallel building [is not safe during -uploadArchives](https://issues.gradle.org/browse/GRADLE-3420). -```bash -grpc-java$ ./gradlew clean build && ./gradlew -Dorg.gradle.parallel=false uploadArchives -``` - -If the version has the `-SNAPSHOT` suffix, the artifacts will automatically -go to the snapshot repository. Otherwise it's a release deployment and the -artifacts will go to a freshly created staging repository. - -### Deploy GRPC Codegen for Additional Platforms -The previous step will only deploy the codegen artifacts for the OS you run on -it and the architecture of your JVM. For a fully fledged deployment, you will -need to deploy the codegen for all other supported OSes and architectures. - -To deploy the codegen for an OS and architecture, you must run the following -commands on that OS and specify the architecture by the flag `-PtargetArch=`. - -If you are doing a snapshot deployment: + # Change version to next patch and add -SNAPSHOT + sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_GRPC_VERSION\)/'$MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT'\1/' \ + "${VERSION_FILES[@]}" + sed -i s/$MAJOR.$MINOR.$PATCH/$MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT/ \ + compiler/src/test{,Lite}/golden/Test{,Deprecated}Service.java.txt + ./gradlew build + git commit -a -m "Bump version to $MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT" + git push -u origin release-v$MAJOR.$MINOR.$PATCH + ``` + Raise a PR and set the base branch of the PR to v$MAJOR.$MINOR.x of the upstream grpc-java repo. +6. Go through PR review and push the release tag and updated release branch to + GitHub (DO NOT click the merge button on the GitHub page): + + ```bash + git checkout v$MAJOR.$MINOR.x + git merge --ff-only release-v$MAJOR.$MINOR.$PATCH + git push upstream v$MAJOR.$MINOR.x + git push upstream v$MAJOR.$MINOR.$PATCH + ``` +7. Close the release milestone. + +8. Trigger build as described in "Auto releasing using kokoro" at + go/grpc-java/releasing. + + It runs three jobs on Kokoro, one on each platform. See their scripts: + `linux_artifacts.sh`, `windows.bat`, and `macos.sh`. The mvn-artifacts/ + outputs of each script is combined into a single folder and then processed + by `upload_artifacts.sh`, which signs the files and uploads to Sonatype. + +9. Once all of the artifacts have been pushed to the staging repository, the + repository should have been closed by `upload_artifacts.sh`. Closing triggers + several sanity checks on the repository. If this completes successfully, the + repository can then be `released`, which will begin the process of pushing + the new artifacts to Maven Central (the staging repository will be destroyed + in the process). You can see the complete process for releasing to Maven + Central on the [OSSRH site](https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#deploying). + +10. We have containers for each release to detect compatibility regressions with + old releases. Generate one for the new release by following the [GCR image + generation instructions][gcr-image]. Summary: + ```bash + # If you haven't previously configured docker: + gcloud auth configure-docker us-docker.pkg.dev + + # In main grpc repo, add the new version to matrix + ${EDITOR:-nano -w} tools/interop_matrix/client_matrix.py + tools/interop_matrix/create_matrix_images.py --git_checkout --release=v$MAJOR.$MINOR.$PATCH \ + --upload_images --language java + docker pull us-docker.pkg.dev/grpc-testing/testing-images-public/grpc_interop_java:v$MAJOR.$MINOR.$PATCH + docker_image=us-docker.pkg.dev/grpc-testing/testing-images-public/grpc_interop_java:v$MAJOR.$MINOR.$PATCH \ + tools/interop_matrix/testcases/java__master + + # Commit the changes + git commit --all -m "[interop] Add grpc-java $MAJOR.$MINOR.$PATCH to client_matrix.py" + + # Create a PR with the `release notes: no` label and run ad-hoc test against your PR + ``` +[gcr-image]: https://github.com/grpc/grpc/blob/master/tools/interop_matrix/README.md#step-by-step-instructions-for-adding-a-gcr-image-for-a-new-release-for-compatibility-test + +11. Update gh-pages with the new Javadoc. Generally the file is on repo1 + 15 minutes after publishing: + + ```bash + git checkout gh-pages + git pull --ff-only upstream gh-pages + rm -r javadoc/ + wget -O grpc-all-javadoc.jar "https://repo1.maven.org/maven2/io/grpc/grpc-all/$MAJOR.$MINOR.$PATCH/grpc-all-$MAJOR.$MINOR.$PATCH-javadoc.jar" + unzip -d javadoc grpc-all-javadoc.jar + patch -p1 < ga.patch + rm grpc-all-javadoc.jar + rm -r javadoc/META-INF/ + git add -A javadoc + git commit -m "Javadoc for $MAJOR.$MINOR.$PATCH" + git push upstream gh-pages + ``` + + Verify the current version is [live on grpc.io](https://grpc.io/grpc-java/javadoc/). + +12. Add [Release Notes](https://github.com/grpc/grpc-java/releases) for the new tag. + *Make sure that any backports are reflected in the release notes.* + +13. Notify the Community. Post a release announcement to + [grpc-io](https://groups.google.com/forum/#!forum/grpc-io) + (`grpc-io@googlegroups.com`) with the title `gRPC-Java v$MAJOR.$MINOR.$PATCH + Released`. The email content should link to the GitHub release notes and + include a copy of them. + +14. Update README.md. Cherry-pick the commit that updated the README.md into the + master branch. + + ```bash + git checkout -b bump-readme master + git cherry-pick v$MAJOR.$MINOR.$PATCH^ + git push --set-upstream origin bump-readme + ``` + + Create a PR and go through the review process + +15. Update version referenced by tutorials. Update `params.grpc_vers.java` in + [config.yaml](https://github.com/grpc/grpc.io/blob/master/config.yaml) of + the grpc.io repository. Create a PR and go through the review process. + +Post-release upgrades +--------------------- +Upgrade dependencies after the release so they can be well-tested before the +next release. -```bash -grpc-java$ ./gradlew clean grpc-compiler:build grpc-compiler:uploadArchives \ - -PtargetArch= -Dorg.gradle.parallel=false -``` +Upgrade the Gradle plugins in `settings.gradle` and the Gradle version in +`gradle/wrapper/gradle-wrapper.properties`. Make sure to read the release notes +for each dependency upgraded. Test by doing a regular build. -When deploying a Release, the first deployment will create -[a new staging repository](https://oss.sonatype.org/#stagingRepositories). You'll need -to look up the ID in the OSSRH UI (usually in the form of `iogrpc-*`). Codegen -deployment commands should include `-PrepositoryId=` in order to -ensure that the artifacts are pushed to the same staging repository. +Upgrade the regular dependencies in `gradle/libs.versions.toml`, except for +Netty and netty-tcnative. To find available upgrades: ```bash -grpc-java$ ./gradlew clean grpc-compiler:build grpc-compiler:uploadArchives -PtargetArch= \ - -PrepositoryId= -Dorg.gradle.parallel=false +./gradlew checkForUpdates ``` -Releasing on Maven Central --------------------------- -Once all of the artifacts have been pushed to the staging repository, the -repository must first be `closed`, which will trigger several sanity checks -on the repository. If this completes successfully, the repository can then -be `released`, which will begin the process of pushing the new artifacts to -Maven Central (the staging repository will be destroyed in the process). You can -see the complete process for releasing to Maven Central on the [OSSRH site] -(http://central.sonatype.org/pages/releasing-the-deployment.html). - -Update README.md ----------------- -After waiting ~1 day and verifying that the release appears on [Maven Central] -(http://mvnrepository.com/), cherry-pick the commit that updated the README into -the master branch and go through review process. - -``` -$ git checkout -b bump-readme master -$ git cherry-pick v$MAJOR.$MINOR.$PATCH^ -``` - -Update version referenced by tutorials --------------------------------------- - -Update the `grpc_java_release_tag` in -[\_data/config.yml](https://github.com/grpc/grpc.github.io/blob/master/_data/config.yml) -of the grpc.github.io repository. - -Notify the Community --------------------- -Finally, document and publicize the release. - -1. Add [Release Notes](https://github.com/grpc/grpc-java/releases) for the new tag. - The description should include any major fixes or features since the last release. - You may choose to add links to bugs, PRs, or commits if appropriate. -2. Post a release announcement to [grpc-io](https://groups.google.com/forum/#!forum/grpc-io) - (`grpc-io@googlegroups.com`). The title should be something that clearly identifies - the release (e.g.`GRPC-Java Released`). - -Update Hosted Javadoc ---------------------- - -Now we need to update gh-pages with the new Javadoc: - -```bash -git checkout gh-pages -rm -r javadoc/ -wget -O grpc-all-javadoc.jar "http://search.maven.org/remotecontent?filepath=io/grpc/grpc-all/$MAJOR.$MINOR.$PATCH/grpc-all-$MAJOR.$MINOR.$PATCH-javadoc.jar" -unzip -d javadoc grpc-all-javadoc.jar -rm grpc-all-javadoc.jar -rm -r javadoc/META-INF/ -git add -A javadoc -git commit -m "Javadoc for $MAJOR.$MINOR.$PATCH" -``` +Test by doing a regular build. For each step, if a dependency cannot be +upgraded, add a comment. Create issues in other projects for breakages, and in +gRPC for things that will need a migration effort. -Push gh-pages to the main repository and verify the current version is [live -on grpc.io](http://www.grpc.io/grpc-java/javadoc/). +When happy with the dependency upgrades, update the versions in `MODULE.bazel`, +`repositories.bzl`, and the various `pom.xml` and `build.gradle` files in +`examples/`. diff --git a/SECURITY.md b/SECURITY.md index 1ab534e204f..fa5b85c0e3a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,113 +1,156 @@ +# Security Policy + +For information on gRPC Security Policy and reporting potentional security +issues, please see [gRPC CVE Process][]. + +[gRPC CVE Process]: https://github.com/grpc/proposal/blob/master/P4-grpc-cve-process.md + # Authentication -gRPC supports a number of different mechanisms for asserting identity between an client and server. This document provides code samples demonstrating how to provide SSL/TLS encryption support and identity assertions in Java, as well as passing OAuth2 tokens to services that support it. +gRPC supports a number of different mechanisms for asserting identity between an +client and server. This document provides code samples demonstrating how to +provide SSL/TLS encryption support and identity assertions in Java, as well as +passing OAuth2 tokens to services that support it. # Transport Security (TLS) -HTTP/2 over TLS mandates the use of [ALPN](https://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg-05) to negotiate the use of the h2 protocol. ALPN is a fairly new standard and (where possible) gRPC also supports protocol negotiation via [NPN](https://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04) for systems that do not yet support ALPN. +HTTP/2 over TLS mandates the use of [ALPN](https://tools.ietf.org/html/rfc7301) +to negotiate the use of the h2 protocol and support for the GCM mode of AES. -On Android, use the [Play Services Provider](#tls-on-android). For non-Android systems, use [OpenSSL](#tls-with-openssl). +There are multiple options available, but on Android we recommend using the +[Play Services Provider](#tls-on-android) and for non-Android systems we +recommend [netty-tcnative with +BoringSSL](#tls-with-netty-tcnative-on-boringssl). ## TLS on Android -On Android we recommend the use of the [Play Services Dynamic Security Provider](http://appfoundry.be/blog/2014/11/18/Google-Play-Services-Dynamic-Security-Provider) to ensure your application has an up-to-date OpenSSL library with the necessary ciper-suites and a reliable ALPN implementation. +On Android we recommend the use of the [Play Services Dynamic Security +Provider][] to ensure your application has an up-to-date OpenSSL library with +the necessary cipher-suites and a reliable ALPN implementation. This requires +[updating the security provider at runtime][config-psdsp]. + +Although ALPN mostly works on newer Android releases (especially since 5.0), +there are bugs and discovered security vulnerabilities that are only fixed by +upgrading the security provider. Thus, we recommend using the Play Service +Dynamic Security Provider for all Android versions. + +*Note: The Dynamic Security Provider must be installed **before** creating a +gRPC OkHttp channel. gRPC statically initializes the security protocol(s) +available, which means that changes to the security provider after the first +channel is created will not be noticed by gRPC.* + +[Play Services Dynamic Security Provider]: https://www.appfoundry.be/blog/2014/11/18/Google-Play-Services-Dynamic-Security-Provider/ +[config-psdsp]: https://developer.android.com/training/articles/security-gms-provider.html -You may need to [update the security provider](https://developer.android.com/training/articles/security-gms-provider.html) to enable ALPN support, especially for Android versions < 5.0. If the provider fails to update, ALPN may not work. +### Bundling Conscrypt -## TLS with OpenSSL +If depending on Play Services is not an option for your app, then you may bundle +[Conscrypt](https://conscrypt.org) with your application. Binaries are available +on [Maven Central][conscrypt-maven]. + +Like the Play Services Dynamic Security Provider, you must still "install" +Conscrypt before use. + +```java +import org.conscrypt.Conscrypt; +import java.security.Security; +... + +Security.insertProviderAt(Conscrypt.newProvider(), 1); +``` -This is currently the recommended approach for using gRPC over TLS (on non-Android systems). +[conscrypt-maven]: https://search.maven.org/#search%7Cga%7C1%7Cg%3Aorg.conscrypt%20a%3Aconscrypt-android -The main benefits of using OpenSSL are: +## TLS on non-Android -1. **Speed**: In local testing, we've seen performance improvements of 3x over the JDK. GCM, which is used by the only cipher suite required by the HTTP/2 spec, is 10-500x faster. -2. **Ciphers**: OpenSSL has its own ciphers and is not dependent on the limitations of the JDK. This allows supporting GCM on Java 7. -3. **ALPN to NPN Fallback**: if the remote endpoint doesn't support ALPN. -4. **Version Independence**: does not require using a different library version depending on the JDK update. +OpenJDK versions prior to Java 8u252 do not support ALPN. Java 8 has 10% the +performance of OpenSSL. -Support for OpenSSL is only provided for the Netty transport via [netty-tcnative](https://github.com/netty/netty-tcnative), which is a fork of -[Apache Tomcat's tcnative](http://tomcat.apache.org/native-doc/), a JNI wrapper around OpenSSL. +We recommend most users use grpc-netty-shaded, which includes netty-tcnative on +BoringSSL. It includes pre-built libraries for 64 bit Windows, OS X, and 64 bit +Linux. For 32 bit Windows, Conscrypt is an option. For all other platforms, Java +9+ is required. -### OpenSSL: Dynamic vs Static (which to use?) +For users of xDS management protocol, the grpc-netty-shaded transport is +particularly appropriate since it is already used internally for the xDS +protocol and is a runtime dependency of grpc-xds. -As of version `1.1.33.Fork14`, netty-tcnative provides two options for usage: statically or dynamically linked. For simplification of initial setup, -we recommend that users first look at `netty-tcnative-boringssl-static`, which is statically linked against BoringSSL and Apache APR. Using this artifact requires no extra installation and guarantees that ALPN and the ciphers required for -HTTP/2 are available. In addition, starting with `1.1.33.Fork16` binaries for -all supported platforms can be included at compile time and the correct binary -for the platform can be selected at runtime. +For users of grpc-netty we recommend [netty-tcnative with +BoringSSL](#tls-with-netty-tcnative-on-boringssl), although using the built-in +JDK support in Java 9+, [Conscrypt](#tls-with-conscrypt), and [netty-tcnative +with OpenSSL](#tls-with-netty-tcnative-on-openssl) are other valid options. -Production systems, however, may require an easy upgrade path for OpenSSL security patches. In this case, relying on the statically linked artifact also implies waiting for the Netty team -to release the new artifact to Maven Central, which can take some time. A better solution in this case is to use the dynamically linked `netty-tcnative` artifact, which allows the site administrator -to easily upgrade OpenSSL in the standard way (e.g. apt-get) without relying on any new builds from Netty. +[Netty TCNative](https://github.com/netty/netty-tcnative) is a fork of +[Apache Tomcat's tcnative](https://tomcat.apache.org/native-doc/) and is a JNI +wrapper around OpenSSL/BoringSSL/LibreSSL. -### OpenSSL: Statically Linked (netty-tcnative-boringssl-static) +We recommend BoringSSL for its simplicity and low occurrence of security +vulnerabilities relative to OpenSSL. BoringSSL is used by Conscrypt as well. -This is the simplest way to configure the Netty transport for OpenSSL. You just need to add the appropriate `netty-tcnative-boringssl-static` artifact to your application's classpath. +### TLS with netty-tcnative on BoringSSL -Artifacts are available on [Maven Central](http://repo1.maven.org/maven2/io/netty/netty-tcnative-boringssl-static/) for the following platforms: +Netty-tcnative with BoringSSL includes BoringSSL statically linked in the +binary. This means the system's pre-installed TLS libraries will not be used. +Production systems that have centralized upgrade agility in the face of +security vulnerabilities may want to use [netty-tcnative on +OpenSSL](#tls-with-netty-tcnative-on-openssl) instead. -Maven Classifier | Description ----------------- | ----------- -windows-x86_64 | Windows distribution -osx-x86_64 | Mac distribution -linux-x86_64 | Linux distribution +Users of grpc-netty-shaded will automatically use netty-tcnative with +BoringSSL. -##### Getting netty-tcnative-boringssl-static from Maven +grpc-netty users will need to add the appropriate +`netty-tcnative-boringssl-static` artifact to the application's classpath. +Artifacts are available for 64 bit Windows, OS X, and 64 bit Linux. -In Maven, you can use the [os-maven-plugin](https://github.com/trustin/os-maven-plugin) to help simplify the dependency. +Depending on netty-tcnative-boringssl-static will include binaries for all +supported platforms. For Maven: ```xml - io.netty netty-tcnative-boringssl-static - 1.1.33.Fork26 + 2.0.20.Final + runtime - ``` -##### Getting netty-tcnative-boringssl-static from Gradle - -Gradle you can use the [osdetector-gradle-plugin](https://github.com/google/osdetector-gradle-plugin), which is a wrapper around the os-maven-plugin. +And for Gradle: ```gradle -buildscript { - repositories { - mavenCentral() - } -} - dependencies { - compile 'io.netty:netty-tcnative-boringssl-static:1.1.33.Fork26' + // See table for correct version + runtime 'io.netty:netty-tcnative-boringssl-static:2.0.20.Final' } ``` -### OpenSSL: Dynamically Linked (netty-tcnative) - -If for any reason you need to dynamically link against OpenSSL (e.g. you need control over the version of OpenSSL), you can instead use the `netty-tcnative` artifact. +For projects sensitive to binary size, specify the classifier for the precise +platform you need: `windows-x86_64`, `osx-x86_64`, `linux-x86_64`. You can also +use [os-maven-plugin](https://github.com/trustin/os-maven-plugin) or +[osdetector-gradle-plugin](https://github.com/google/osdetector-gradle-plugin), +to choose the classifier for the platform running the build. -Requirements: +### TLS with netty-tcnative on OpenSSL -1. [OpenSSL](https://www.openssl.org/) version >= 1.0.2 for ALPN support, or version >= 1.0.1 for NPN. -2. [Apache APR library (libapr-1)](https://apr.apache.org/) version >= 1.5.2. -3. [netty-tcnative](https://github.com/netty/netty-tcnative) version >= 1.1.33.Fork7 must be on classpath. Prior versions only supported NPN and only Fedora-derivatives were supported for Linux. - -Artifacts are available on [Maven Central](http://repo1.maven.org/maven2/io/netty/netty-tcnative/) for the following platforms: +Using OpenSSL can have more initial configuration issues, but can be useful if +your OS's OpenSSL version is recent and kept up-to-date with security fixes. +OpenSSL is not included with tcnative, but instead is dynamically linked using +your operating system's OpenSSL. -Classifier | Description ----------------- | ----------- -windows-x86_64 | Windows distribution -osx-x86_64 | Mac distribution -linux-x86_64 | Used for non-Fedora derivatives of Linux -linux-x86_64-fedora | Used for Fedora derivatives +To use OpenSSL you will use the `netty-tcnative` artifact. It requires: -On Linux it should be noted that OpenSSL uses a different soname for Fedora derivatives than other Linux releases. To work around this limitation, netty-tcnative deploys two separate versions for linux. +1. [OpenSSL](https://www.openssl.org/) version >= 1.0.2 for ALPN support. +2. [Apache APR library (libapr-1)](https://apr.apache.org/) version >= 1.5.2. -##### Getting netty-tcnative from Maven +You must specify a classifier for the correct netty-tcnative binary: +`windows-x86_64`, `osx-x86_64`, `linux-x86_64`, or `linux-x86_64-fedora`. +Fedora derivatives use a different soname from other Linux distributations, so +you must select the "fedora" version on those distributions. -In Maven, you can use the [os-maven-plugin](https://github.com/trustin/os-maven-plugin) to help simplify the dependency. +In Maven, you can use the +[os-maven-plugin](https://github.com/trustin/os-maven-plugin) to help simplify +the dependency. ```xml @@ -115,8 +158,9 @@ In Maven, you can use the [os-maven-plugin](https://github.com/trustin/os-maven- io.netty netty-tcnative - 1.1.33.Fork26 + 2.0.20.Final ${tcnative.classifier} + runtime @@ -126,7 +170,7 @@ In Maven, you can use the [os-maven-plugin](https://github.com/trustin/os-maven- kr.motd.maven os-maven-plugin - 1.4.0.Final + 1.7.1 @@ -158,9 +202,8 @@ In Maven, you can use the [os-maven-plugin](https://github.com/trustin/os-maven- ``` -##### Getting netty-tcnative from Gradle - -Gradle you can use the [osdetector-gradle-plugin](https://github.com/google/osdetector-gradle-plugin), which is a wrapper around the os-maven-plugin. +And in Gradle you can use the +[osdetector-gradle-plugin](https://github.com/google/osdetector-gradle-plugin). ```gradle buildscript { @@ -183,58 +226,34 @@ if (osdetector.os == "linux" && osdetector.release.isLike("fedora")) { } dependencies { - compile 'io.netty:netty-tcnative:1.1.33.Fork26:' + tcnative_classifier + runtime 'io.netty:netty-tcnative:2.0.20.Final:' + tcnative_classifier } ``` -## TLS with JDK (Jetty ALPN/NPN) - -**WARNING: DON'T DO THIS!!** - -*For non-Android systems, the recommended approach is to use [OpenSSL](#tls-with-openssl). Using the JDK for ALPN is generally much slower and may not support the necessary ciphers for HTTP2.* +### TLS with Conscrypt -*Jetty ALPN brings its own baggage in that the Java bootclasspath needs to be modified, which may not be an option for some environments. In addition, a specific version of Jetty ALPN has to be used for a given version of the JRE. If the versions don't match the negotiation will fail, but you won't really know why. And since there is such a tight coupling between Jetty ALPN and the JRE, there are no guarantees that Jetty ALPN will support every JRE out in the wild.* +[Conscrypt](https://conscrypt.org) provides an implementation of the JSSE +security APIs based on BoringSSL. Pre-built binaries are available for 32 and +64 bit Windows, OS X, and 64 bit Linux. -*The moral of the story is: Don't use the JDK for ALPN! But if you absolutely have to, here's how you do it... :)* +Depend on `conscrypt-openjdk-uber` for binaries of all supported JRE platforms. +For projects sensitive to binary size, depend on `conscrypt-openjdk` and +specify the appropriate classifier. `os-maven-plugin` and +`osdetector-gradle-plugin` may also be used. See the documentation for +[netty-tcnative-boringssl-static](#tls-with-netty-tcnative-on-boringssl) for +example usage of the plugins. ---- +Generally you will "install" Conscrypt before use, for gRPC to find. -If not using the Netty transport (or you are unable to use OpenSSL for some reason) another alternative is to use the JDK for TLS. - -No standard Java release has built-in support for ALPN today ([there is a tracking issue](https://bugs.openjdk.java.net/browse/JDK-8051498) so go upvote it!) so we need to use the [Jetty-ALPN](https://github.com/jetty-project/jetty-alpn) (or [Jetty-NPN](https://github.com/jetty-project/jetty-npn) if on Java < 8) bootclasspath extension for OpenJDK. To do this, add an `Xbootclasspath` JVM option referencing the path to the Jetty `alpn-boot` jar. - -```sh -java -Xbootclasspath/p:/path/to/jetty/alpn/extension.jar ... -``` - -Note that you must use the [release of the Jetty-ALPN jar](http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-versions) specific to the version of Java you are using. However, you can use the JVM agent [Jeety-ALPN-Agent](https://github.com/jetty-project/jetty-alpn-agent) to load the correct Jetty `alpn-boot` jar file for the current Java version. To do this, instead of adding an `Xbootclasspath` option, add a `javaagent` JVM option referencing the path to the Jetty `alpn-agent` jar. +```java +import org.conscrypt.Conscrypt; +import java.security.Security; +... -```sh -java -javaagent:/path/to/jetty-alpn-agent.jar ... +// Somewhere in main() +Security.insertProviderAt(Conscrypt.newProvider(), 1); ``` -### JDK Ciphers - -Java 7 does not support [the cipher suites recommended](https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-9.2.2) by the HTTP2 specification. To address this we suggest servers use Java 8 where possible or use an alternative JCE implementation such as [Bouncy Castle](https://www.bouncycastle.org/java.html). If this is not practical it is possible to use other ciphers but you need to ensure that the services you intend to call have [allowed out-of-spec ciphers](https://github.com/grpc/grpc/issues/681) and have evaluated the security risks of doing so. - -Users should be aware that GCM is [_very_ slow (1 MB/s)](https://bugzilla.redhat.com/show_bug.cgi?id=1135504) before Java 8u60. With Java 8u60 GCM is 10x faster (10-20 MB/s), but that is still slow compared to OpenSSL (~200 MB/s), especially with AES-NI support (~1 GB/s). GCM cipher suites are the only suites available that comply with HTTP2's cipher requirements. - -### Configuring Jetty ALPN in Web Containers - -Some web containers, such as [Jetty](http://www.eclipse.org/jetty/documentation/current/jetty-classloading.html) restrict access to server classes for web applications. A gRPC client running within such a container must be properly configured to allow access to the ALPN classes. In Jetty, this is done by including a `WEB-INF/jetty-env.xml` file containing the following: - -```xml - - - - - - - - -org.eclipse.jetty.alpn. - - -``` ## Enabling TLS on a server To use TLS on the server, a certificate chain and private key need to be @@ -242,49 +261,156 @@ specified in PEM format. The standard TLS port is 443, but we use 8443 below to avoid needing extra permissions from the OS. ```java -Server server = ServerBuilder.forPort(8443) - // Enable TLS - .useTransportSecurity(certChainFile, privateKeyFile) +ServerCredentials creds = TlsServerCredentials.create(certChainFile, privateKeyFile); +Server server = Grpc.newServerBuilderForPort(8443, creds) .addService(serviceImplementation) - .build(); -server.start(); + .build() + .start(); ``` If the issuing certificate authority is not known to the client then a properly -configured SslContext or SSLSocketFactory should be provided to the -NettyChannelBuilder or OkHttpChannelBuilder, respectively. +configured trust manager should be provided to TlsChannelCredentials and used to +construct the channel. ## Mutual TLS [Mutual authentication][] (or "client-side authentication") configuration is similar to the server by providing truststores, a client certificate and private key to the client channel. The server must also be configured to request a certificate from clients, as well as truststores for which client certificates it should allow. ```java -Server server = NettyServerBuilder.forPort(8443) - .sslContext(GrpcSslContexts.forServer(certChainFile, privateKeyFile) - .trustManager(clientCertChainFile) - .clientAuth(ClientAuth.OPTIONAL) - .build()); +ServerCredentials creds = TlsServerCredentials.newBuilder() + .keyManager(certChainFile, privateKeyFile) + .trustManager(clientCAsFile) + .clientAuth(TlsServerCredentials.ClientAuth.REQUIRE) + .build(); ``` -Negotiated client certificates are available in the SSLSession, which is found in the SSL_SESSION_KEY attribute of ServerCall. A server interceptor can provide details in the current Context. +Negotiated client certificates are available in the SSLSession, which is found +in the `Grpc.TRANSPORT_ATTR_SSL_SESSION` attribute of the call. A server +interceptor can provide details in the current Context. ```java -public final static Context.Key SSL_SESSION_CONTEXT = Context.key("SSLSession"); +// The application uses this in its handlers. +public static final Context.Key SECURITY_INFO = Context.key("my.security.Info"); @Override -public ServerCall.Listener interceptCall(ServerCall call, +public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { - SSLSession sslSession = call.attributes().get(ServerCall.SSL_SESSION_KEY); + SSLSession sslSession = call.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION); if (sslSession == null) { - return next.startCall(call, headers) + return next.startCall(call, headers); } + // This interceptor can provide a centralized policy to process the client's + // certificate. Avoid exposing low-level details (like SSLSession) and + // instead provide a higher-level concept like "authenticated user." + MySecurityInfo info = process(sslSession); return Contexts.interceptCall( - Context.current().withValue(SSL_SESSION_CONTEXT, clientContext), call, headers, next); + Context.current().withValue(SECURITY_INFO, info), call, headers, next); } ``` [Mutual authentication]: http://en.wikipedia.org/wiki/Transport_Layer_Security#Client-authenticated_TLS_handshake +## Troubleshooting + +If you received an error message "ALPN is not configured properly" or "Jetty ALPN/NPN has not been properly configured", it most likely means that: + - ALPN related dependencies are either not present in the classpath + - or that there is a classpath conflict + - or that a wrong version is used due to dependency management + - or you are on an unsupported platform (e.g., 32-bit OS). See [Transport + Security](#transport-security-tls) for supported platforms. + +### Netty +If you aren't using gRPC on Android devices, you are most likely using `grpc-netty` transport. + +If you are developing for Android and have a dependency on `grpc-netty`, you should remove it as `grpc-netty` is unsupported on Android. Use `grpc-okhttp` instead. + +If you are on a 32-bit operating system, using Java 11+ may be the easiest +solution, as ALPN was added to Java in Java 9. If on 32-bit Windows, [Conscrypt +is an option](#tls-with-conscrypt). Otherwise you need to [build your own 32-bit +version of +`netty-tcnative`](https://netty.io/wiki/forked-tomcat-native.html#wiki-h2-6). + +If on Alpine Linux, depending on your specific JDK you may see a crash in +netty_tcnative. This is generally caused by a missing symbol. Run `apk install +gcompat` and use the environment variable `LD_PRELOAD=/lib/libgcompat.so.0` when +executing Java. + +If on Fedora 30 or later and you see "libcrypt.so.1: cannot open shared object +file: No such file or directory". Run `dnf -y install libxcrypt-compat` to +install the necessary dependency. + +Most dependency versioning problems can be solved by using +`io.grpc:grpc-netty-shaded` instead of `io.grpc:grpc-netty`, although this also +limits your usage of the Netty-specific APIs. `io.grpc:grpc-netty-shaded` +includes the proper version of Netty and `netty-tcnative-boringssl-static` in a +way that won't conflict with other Netty usages. + +Find the dependency tree (e.g., `mvn dependency:tree`), and look for versions of: + - `io.grpc:grpc-netty` + - `io.netty:netty-handler` (really, make sure all of io.netty except for + netty-tcnative has the same version) + - `io.netty:netty-tcnative-boringssl-static:jar` + +If `netty-tcnative-boringssl-static` is missing, then you either need to add it as a dependency, or use alternative methods of providing ALPN capability by reading the *Transport Security (TLS)* section carefully. + +If you have both `netty-handler` and `netty-tcnative-boringssl-static` dependencies, then check the versions carefully. These versions could've been overridden by dependency management from another BOM. You would receive the "ALPN is not configured properly" exception if you are using incompatible versions. + +If you have other `netty` dependencies, such as `netty-all`, that are pulled in from other libraries, then ultimately you should make sure only one `netty` dependency is used to avoid classpath conflict. The easiest way is to exclude transitive Netty dependencies from all the immediate dependencies, e.g., in Maven use ``, and then add an explict Netty dependency in your project along with the corresponding `tcnative` versions. See the versions table below. + +If you are running in a runtime environment that also uses Netty (e.g., Hadoop, Spark, Spring Boot 2) and you have no control over the Netty version at all, then you should use a shaded gRPC Netty dependency to avoid classpath conflicts with other Netty versions in runtime the classpath: + - Remove `io.grpc:grpc-netty` dependency + - Add `io.grpc:grpc-netty-shaded` dependency + +Below are known to work version combinations: + +grpc-netty version | netty-handler version | netty-tcnative-boringssl-static version +------------------ |-----------------------| --------------------------------------- +1.0.0-1.0.1 | 4.1.3.Final | 1.1.33.Fork19 +1.0.2-1.0.3 | 4.1.6.Final | 1.1.33.Fork23 +1.1.x-1.3.x | 4.1.8.Final | 1.1.33.Fork26 +1.4.x | 4.1.11.Final | 2.0.1.Final +1.5.x | 4.1.12.Final | 2.0.5.Final +1.6.x | 4.1.14.Final | 2.0.5.Final +1.7.x-1.8.x | 4.1.16.Final | 2.0.6.Final +1.9.x-1.10.x | 4.1.17.Final | 2.0.7.Final +1.11.x-1.12.x | 4.1.22.Final | 2.0.7.Final +1.13.x | 4.1.25.Final | 2.0.8.Final +1.14.x-1.15.x | 4.1.27.Final | 2.0.12.Final +1.16.x-1.17.x | 4.1.30.Final | 2.0.17.Final +1.18.x-1.19.x | 4.1.32.Final | 2.0.20.Final +1.20.x-1.21.x | 4.1.34.Final | 2.0.22.Final +1.22.x | 4.1.35.Final | 2.0.25.Final +1.23.x-1.24.x | 4.1.38.Final | 2.0.25.Final +1.25.x-1.27.x | 4.1.42.Final | 2.0.26.Final +1.28.x | 4.1.45.Final | 2.0.28.Final +1.29.x-1.31.x | 4.1.48.Final | 2.0.30.Final +1.32.x-1.34.x | 4.1.51.Final | 2.0.31.Final +1.35.x-1.41.x | 4.1.52.Final | 2.0.34.Final +1.42.x-1.43.x | 4.1.63.Final | 2.0.38.Final +1.44.x-1.47.x | 4.1.72.Final | 2.0.46.Final +1.48.x-1.49.x | 4.1.77.Final | 2.0.53.Final +1.50.x-1.53.x | 4.1.79.Final | 2.0.54.Final +1.54.x-1.55.x | 4.1.87.Final | 2.0.56.Final +1.56.x | 4.1.87.Final | 2.0.61.Final +1.57.x-1.58.x | 4.1.93.Final | 2.0.61.Final +1.59.x | 4.1.97.Final | 2.0.61.Final +1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final +1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final +1.71.x-1.74.x | 4.1.110.Final | 2.0.70.Final +1.75.x-1.76.x | 4.1.124.Final | 2.0.72.Final +1.77.x-1.78.x | 4.1.127.Final | 2.0.74.Final +1.79.x- | 4.1.130.Final | 2.0.74.Final + +_(grpc-netty-shaded avoids issues with keeping these versions in sync.)_ + +### OkHttp +If you are using gRPC on Android devices, you are most likely using +`grpc-okhttp` transport. + +Find the dependency tree (e.g., `mvn dependency:tree`), and look for +`io.grpc:grpc-okhttp`. If you don't have `grpc-okhttp`, you should add it as a +dependency. + # gRPC over plaintext An option is provided to use gRPC over plaintext without TLS. While this is convenient for testing environments, users must be aware of the security risks of doing so for real production systems. @@ -294,17 +420,12 @@ An option is provided to use gRPC over plaintext without TLS. While this is conv The following code snippet shows how you can call the Google Cloud PubSub API using gRPC with a service account. The credentials are loaded from a key stored in a well-known location or by detecting that the application is running in an environment that can provide one automatically, e.g. Google Compute Engine. While this example is specific to Google and it's services, similar patterns can be followed for other service providers. ```java -// Create a channel to the test service. -ManagedChannel channel = ManagedChannelBuilder.forTarget("pubsub.googleapis.com") +// Use the default credentials from the environment +ChannelCredentials creds = GoogleDefaultChannelCredentials.create(); +// Create a channel to the service +ManagedChannel channel = Grpc.newChannelBuilder("dns:///pubsub.googleapis.com", creds) .build(); -// Get the default credentials from the environment -GoogleCredentials creds = GoogleCredentials.getApplicationDefault(); -// Down-scope the credential to just the scopes required by the service -creds = creds.createScoped(Arrays.asList("https://www.googleapis.com/auth/pubsub")); -// Create an instance of {@link io.grpc.CallCredentials} -CallCredentials callCreds = MoreCallCredentials.from(creds); -// Create a stub with credential -PublisherGrpc.PublisherBlockingStub publisherStub = - PublisherGrpc.newBlockingStub(channel).withCallCredentials(callCreds); +// Create a stub and send an RPC +PublisherGrpc.PublisherBlockingStub publisherStub = PublisherGrpc.newBlockingStub(channel); publisherStub.publish(someMessage); ``` diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 00000000000..1efdf2793a8 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,58 @@ +workspace(name = "io_grpc_grpc_java") + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS", "grpc_java_repositories") + +grpc_java_repositories() + +http_archive( + name = "rules_java", + sha256 = "47632cc506c858011853073449801d648e10483d4b50e080ec2549a4b2398960", + urls = [ + "https://github.com/bazelbuild/rules_java/releases/download/8.15.2/rules_java-8.15.2.tar.gz", + ], +) + +load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "protobuf_deps") + +protobuf_deps() + +load("@rules_java//java:rules_java_deps.bzl", "rules_java_dependencies") + +rules_java_dependencies() + +load("@bazel_features//:deps.bzl", "bazel_features_deps") + +bazel_features_deps() + +load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") + +jar_jar_repositories() + +load("@rules_python//python:repositories.bzl", "py_repositories") + +py_repositories() + +load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") + +switched_rules_by_language( + name = "com_google_googleapis_imports", +) + +http_archive( + name = "rules_jvm_external", + sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac", + strip_prefix = "rules_jvm_external-5.3", + url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz", +) + +load("@rules_jvm_external//:defs.bzl", "maven_install") + +maven_install( + artifacts = IO_GRPC_GRPC_JAVA_ARTIFACTS + PROTOBUF_MAVEN_ARTIFACTS, + override_targets = IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS, + repositories = [ + "https://repo.maven.apache.org/maven2/", + ], + strict_visibility = True, +) diff --git a/WORKSPACE.bzlmod b/WORKSPACE.bzlmod new file mode 100644 index 00000000000..4ecb9e5d985 --- /dev/null +++ b/WORKSPACE.bzlmod @@ -0,0 +1 @@ +# When using bzlmod this makes sure nothing from the legacy WORKSPACE is loaded diff --git a/all/build.gradle b/all/build.gradle index 5a7a28832a6..11eec4b7ff8 100644 --- a/all/build.gradle +++ b/all/build.gradle @@ -1,41 +1,48 @@ -apply plugin: 'com.github.kt3k.coveralls' +plugins { + id "java-library" + id "maven-publish" -description = "gRPC: All" - -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.0.1' - } + id "com.github.kt3k.coveralls" } +description = "gRPC: All" + def subprojects = [ - project(':grpc-auth'), - project(':grpc-core'), - project(':grpc-context'), - project(':grpc-netty'), - project(':grpc-okhttp'), - project(':grpc-protobuf'), - project(':grpc-protobuf-lite'), - project(':grpc-protobuf-nano'), - project(':grpc-stub'), - // project(':grpc-thrift'), // not exported currently. + project(':grpc-api'), + project(':grpc-auth'), + project(':grpc-core'), + project(':grpc-grpclb'), + project(':grpc-gcp-csm-observability'), + project(':grpc-inprocess'), + project(':grpc-netty'), + project(':grpc-okhttp'), + project(':grpc-opentelemetry'), + project(':grpc-protobuf'), + project(':grpc-protobuf-lite'), + project(':grpc-rls'), + project(':grpc-services'), + project(':grpc-servlet'), + project(':grpc-servlet-jakarta'), + project(':grpc-stub'), + project(':grpc-testing'), + project(':grpc-util'), + project(':grpc-xds'), ] -for (subproject in rootProject.subprojects) { +for (subproject in subprojects) { if (subproject == project) { continue } evaluationDependsOn(subproject.path) } +evaluationDependsOn(':grpc-interop-testing') dependencies { - compile subprojects + api subprojects.minus([project(':grpc-protobuf-lite')]) + implementation libraries.guava.jre // JRE required by transitive protobuf-java-util } -javadoc { +tasks.named("javadoc").configure { classpath = files(subprojects.collect { subproject -> subproject.javadoc.classpath }) @@ -44,40 +51,29 @@ javadoc { continue; } source subproject.javadoc.source - options.links subproject.javadoc.options.links.toArray(new String[0]) + options.linksOffline.addAll subproject.javadoc.options.linksOffline } - exclude 'io/grpc/internal/**' } -task jacocoMerge(type: JacocoMerge) { - dependsOn(subprojects.jacocoTestReport.dependsOn) +tasks.named("jacocoTestReport").configure { mustRunAfter(subprojects.jacocoTestReport.mustRunAfter) - destinationFile = file("${buildDir}/jacoco/test.exec") - executionData = files(subprojects.jacocoTestReport.executionData) - .plus(project(':grpc-interop-testing').jacocoTestReport.executionData) - .filter { f -> f.exists() } -} - -jacocoTestReport { - dependsOn(jacocoMerge) + mustRunAfter(project(':grpc-interop-testing').jacocoTestReport.mustRunAfter) + executionData.from files(subprojects.jacocoTestReport.executionData) + .plus(project(':grpc-interop-testing').jacocoTestReport.executionData) reports { - xml.enabled = true - html.enabled = true + xml.required = true + html.required = true } - additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs) - sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs) - classDirectories = files(subprojects.sourceSets.main.output) - classDirectories = files(classDirectories.files.collect { - fileTree(dir: it, - exclude: ['**/io/grpc/okhttp/internal/**']) - }) + subprojects.each { subproject -> + additionalSourceDirs.from(subproject.jacocoTestReport.additionalSourceDirs) + sourceDirectories.from(subproject.jacocoTestReport.sourceDirectories) + classDirectories.from(subproject.jacocoTestReport.classDirectories) + } } coveralls { sourceDirs = subprojects.sourceSets.main.allSource.srcDirs.flatten() } -tasks.coveralls { - dependsOn(jacocoTestReport) -} +tasks.named("coveralls").configure { dependsOn tasks.named("jacocoTestReport") } diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel new file mode 100644 index 00000000000..f29df303fbe --- /dev/null +++ b/alts/BUILD.bazel @@ -0,0 +1,87 @@ +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_java//java:defs.bzl", "java_library") +load("@rules_jvm_external//:defs.bzl", "artifact") +load("//:java_grpc_library.bzl", "java_grpc_library") + +java_library( + name = "alts_internal", + srcs = glob([ + "src/main/java/io/grpc/alts/internal/*.java", + ]), + deps = [ + ":handshaker_java_grpc", + ":handshaker_java_proto", + "//api", + "//core:internal", + "//netty", + "//stub", + "@com_google_protobuf//:protobuf_java", + "@com_google_protobuf//:protobuf_java_util", + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), + artifact("com.google.guava:guava"), + artifact("io.netty:netty-buffer"), + artifact("io.netty:netty-codec"), + artifact("io.netty:netty-common"), + artifact("io.netty:netty-handler"), + artifact("io.netty:netty-transport"), + ], +) + +java_library( + name = "alts", + srcs = glob([ + "src/main/java/io/grpc/alts/*.java", + ]), + visibility = ["//visibility:public"], + deps = [ + ":alts_internal", + ":handshaker_java_grpc", + ":handshaker_java_proto", + "//api", + "//auth", + "//core:internal", + "//netty", + artifact("com.google.auth:google-auth-library-oauth2-http"), + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.guava:guava"), + artifact("io.netty:netty-common"), + artifact("io.netty:netty-handler"), + artifact("io.netty:netty-transport"), + ], +) + +# bazel only accepts proto import with absolute path. +genrule( + name = "protobuf_imports", + srcs = glob(["src/main/proto/grpc/gcp/*.proto"]), + outs = [ + "protobuf_out/grpc/gcp/altscontext.proto", + "protobuf_out/grpc/gcp/handshaker.proto", + "protobuf_out/grpc/gcp/transport_security_common.proto", + ], + cmd = "for fname in $(SRCS); do " + + "sed 's,import \",import \"alts/protobuf_out/,g' $$fname > " + + "$(@D)/protobuf_out/grpc/gcp/$$(basename $$fname); done", +) + +proto_library( + name = "handshaker_proto", + srcs = [ + "protobuf_out/grpc/gcp/altscontext.proto", + "protobuf_out/grpc/gcp/handshaker.proto", + "protobuf_out/grpc/gcp/transport_security_common.proto", + ], +) + +java_proto_library( + name = "handshaker_java_proto", + deps = [":handshaker_proto"], +) + +java_grpc_library( + name = "handshaker_java_grpc", + srcs = [":handshaker_proto"], + deps = [":handshaker_java_proto"], +) diff --git a/alts/build.gradle b/alts/build.gradle new file mode 100644 index 00000000000..c206a37bcef --- /dev/null +++ b/alts/build.gradle @@ -0,0 +1,104 @@ +plugins { + id "java-library" + id "maven-publish" + + id "com.google.protobuf" + id "com.gradleup.shadow" + id "ru.vyarus.animalsniffer" +} + +description = "gRPC: ALTS" + +dependencies { + api project(':grpc-api') + implementation project(':grpc-auth'), + project(':grpc-core'), + project(":grpc-context"), // Override google-auth dependency with our newer version + project(':grpc-protobuf'), + project(':grpc-stub'), + libraries.protobuf.java, + libraries.conscrypt, + libraries.google.auth.oauth2Http + def nettyDependency = implementation project(':grpc-netty') + + shadow configurations.implementation.getDependencies().minus(nettyDependency) + shadow project(path: ':grpc-netty-shaded', configuration: 'shadow') + + testImplementation project(':grpc-testing'), + testFixtures(project(':grpc-core')), + project(':grpc-inprocess'), + project(':grpc-testing-proto'), + libraries.guava, + libraries.junit, + libraries.mockito.core, + libraries.truth + + testImplementation libraries.guava.testlib + testRuntimeOnly libraries.netty.tcnative, + libraries.netty.tcnative.classes + testRuntimeOnly (libraries.netty.transport.epoll) { + artifact { + classifier = "linux-x86_64" + } + } + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } +} + +configureProtoCompilation() + +import net.ltgt.gradle.errorprone.CheckSeverity + +[tasks.named("compileJava"), tasks.named("compileTestJava")]*.configure { + // ALTS returns a lot of futures that we mostly don't care about. + options.errorprone.check("FutureReturnValueIgnored", CheckSeverity.OFF) +} + +tasks.named("javadoc").configure { + exclude 'io/grpc/alts/internal/**' + exclude 'io/grpc/alts/Internal*' +} + +tasks.named("jar").configure { + // Must use a different archiveClassifier to avoid conflicting with shadowJar + archiveClassifier = 'original' + manifest { + attributes('Automatic-Module-Name': 'io.grpc.alts') + } +} + +// We want to use grpc-netty-shaded instead of grpc-netty. But we also want our +// source to work with Bazel, so we rewrite the code as part of the build. +tasks.named("shadowJar").configure { + archiveClassifier = null + dependencies { + exclude(dependency {true}) + } + relocate 'io.grpc.netty', 'io.grpc.netty.shaded.io.grpc.netty' + relocate 'io.netty', 'io.grpc.netty.shaded.io.netty' +} + +publishing { + publications { + maven(MavenPublication) { + // We want this to throw an exception if it isn't working + def originalJar = artifacts.find { dep -> dep.classifier == 'original'} + artifacts.remove(originalJar) + + pom.withXml { + def dependenciesNode = new Node(null, 'dependencies') + project.configurations.shadow.allDependencies.each { dep -> + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', dep.group) + dependencyNode.appendNode('artifactId', dep.name) + dependencyNode.appendNode('version', dep.version) + dependencyNode.appendNode('scope', 'compile') + } + asNode().dependencies[0].replaceNode(dependenciesNode) + } + } + } +} diff --git a/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java new file mode 100644 index 00000000000..07e4256eb75 --- /dev/null +++ b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java @@ -0,0 +1,339 @@ +package io.grpc.alts.internal; + +import static io.grpc.MethodDescriptor.generateFullMethodName; + +/** + */ +@io.grpc.stub.annotations.GrpcGenerated +public final class HandshakerServiceGrpc { + + private HandshakerServiceGrpc() {} + + public static final java.lang.String SERVICE_NAME = "grpc.gcp.HandshakerService"; + + // Static method descriptors that strictly reflect the proto. + private static volatile io.grpc.MethodDescriptor getDoHandshakeMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "DoHandshake", + requestType = io.grpc.alts.internal.HandshakerReq.class, + responseType = io.grpc.alts.internal.HandshakerResp.class, + methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + public static io.grpc.MethodDescriptor getDoHandshakeMethod() { + io.grpc.MethodDescriptor getDoHandshakeMethod; + if ((getDoHandshakeMethod = HandshakerServiceGrpc.getDoHandshakeMethod) == null) { + synchronized (HandshakerServiceGrpc.class) { + if ((getDoHandshakeMethod = HandshakerServiceGrpc.getDoHandshakeMethod) == null) { + HandshakerServiceGrpc.getDoHandshakeMethod = getDoHandshakeMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "DoHandshake")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.grpc.alts.internal.HandshakerReq.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.grpc.alts.internal.HandshakerResp.getDefaultInstance())) + .setSchemaDescriptor(new HandshakerServiceMethodDescriptorSupplier("DoHandshake")) + .build(); + } + } + } + return getDoHandshakeMethod; + } + + /** + * Creates a new async stub that supports all call types for the service + */ + public static HandshakerServiceStub newStub(io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public HandshakerServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceStub(channel, callOptions); + } + }; + return HandshakerServiceStub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static HandshakerServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public HandshakerServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceBlockingV2Stub(channel, callOptions); + } + }; + return HandshakerServiceBlockingV2Stub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports unary and streaming output calls on the service + */ + public static HandshakerServiceBlockingStub newBlockingStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public HandshakerServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceBlockingStub(channel, callOptions); + } + }; + return HandshakerServiceBlockingStub.newStub(factory, channel); + } + + /** + * Creates a new ListenableFuture-style stub that supports unary calls on the service + */ + public static HandshakerServiceFutureStub newFutureStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public HandshakerServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceFutureStub(channel, callOptions); + } + }; + return HandshakerServiceFutureStub.newStub(factory, channel); + } + + /** + */ + public interface AsyncService { + + /** + *
+     * Handshaker service accepts a stream of handshaker request, returning a
+     * stream of handshaker response. Client is expected to send exactly one
+     * message with either client_start or server_start followed by one or more
+     * messages with next. Each time client sends a request, the handshaker
+     * service expects to respond. Client does not have to wait for service's
+     * response before sending next request.
+     * 
+ */ + default io.grpc.stub.StreamObserver doHandshake( + io.grpc.stub.StreamObserver responseObserver) { + return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getDoHandshakeMethod(), responseObserver); + } + } + + /** + * Base class for the server implementation of the service HandshakerService. + */ + public static abstract class HandshakerServiceImplBase + implements io.grpc.BindableService, AsyncService { + + @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { + return HandshakerServiceGrpc.bindService(this); + } + } + + /** + * A stub to allow clients to do asynchronous rpc calls to service HandshakerService. + */ + public static final class HandshakerServiceStub + extends io.grpc.stub.AbstractAsyncStub { + private HandshakerServiceStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected HandshakerServiceStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceStub(channel, callOptions); + } + + /** + *
+     * Handshaker service accepts a stream of handshaker request, returning a
+     * stream of handshaker response. Client is expected to send exactly one
+     * message with either client_start or server_start followed by one or more
+     * messages with next. Each time client sends a request, the handshaker
+     * service expects to respond. Client does not have to wait for service's
+     * response before sending next request.
+     * 
+ */ + public io.grpc.stub.StreamObserver doHandshake( + io.grpc.stub.StreamObserver responseObserver) { + return io.grpc.stub.ClientCalls.asyncBidiStreamingCall( + getChannel().newCall(getDoHandshakeMethod(), getCallOptions()), responseObserver); + } + } + + /** + * A stub to allow clients to do synchronous rpc calls to service HandshakerService. + */ + public static final class HandshakerServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private HandshakerServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected HandshakerServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Handshaker service accepts a stream of handshaker request, returning a
+     * stream of handshaker response. Client is expected to send exactly one
+     * message with either client_start or server_start followed by one or more
+     * messages with next. Each time client sends a request, the handshaker
+     * service expects to respond. Client does not have to wait for service's
+     * response before sending next request.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + doHandshake() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getDoHandshakeMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service HandshakerService. + */ + public static final class HandshakerServiceBlockingStub + extends io.grpc.stub.AbstractBlockingStub { + private HandshakerServiceBlockingStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected HandshakerServiceBlockingStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceBlockingStub(channel, callOptions); + } + } + + /** + * A stub to allow clients to do ListenableFuture-style rpc calls to service HandshakerService. + */ + public static final class HandshakerServiceFutureStub + extends io.grpc.stub.AbstractFutureStub { + private HandshakerServiceFutureStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected HandshakerServiceFutureStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceFutureStub(channel, callOptions); + } + } + + private static final int METHODID_DO_HANDSHAKE = 0; + + private static final class MethodHandlers implements + io.grpc.stub.ServerCalls.UnaryMethod, + io.grpc.stub.ServerCalls.ServerStreamingMethod, + io.grpc.stub.ServerCalls.ClientStreamingMethod, + io.grpc.stub.ServerCalls.BidiStreamingMethod { + private final AsyncService serviceImpl; + private final int methodId; + + MethodHandlers(AsyncService serviceImpl, int methodId) { + this.serviceImpl = serviceImpl; + this.methodId = methodId; + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public void invoke(Req request, io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + default: + throw new AssertionError(); + } + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public io.grpc.stub.StreamObserver invoke( + io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + case METHODID_DO_HANDSHAKE: + return (io.grpc.stub.StreamObserver) serviceImpl.doHandshake( + (io.grpc.stub.StreamObserver) responseObserver); + default: + throw new AssertionError(); + } + } + } + + public static final io.grpc.ServerServiceDefinition bindService(AsyncService service) { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + .addMethod( + getDoHandshakeMethod(), + io.grpc.stub.ServerCalls.asyncBidiStreamingCall( + new MethodHandlers< + io.grpc.alts.internal.HandshakerReq, + io.grpc.alts.internal.HandshakerResp>( + service, METHODID_DO_HANDSHAKE))) + .build(); + } + + private static abstract class HandshakerServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier { + HandshakerServiceBaseDescriptorSupplier() {} + + @java.lang.Override + public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { + return io.grpc.alts.internal.HandshakerProto.getDescriptor(); + } + + @java.lang.Override + public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() { + return getFileDescriptor().findServiceByName("HandshakerService"); + } + } + + private static final class HandshakerServiceFileDescriptorSupplier + extends HandshakerServiceBaseDescriptorSupplier { + HandshakerServiceFileDescriptorSupplier() {} + } + + private static final class HandshakerServiceMethodDescriptorSupplier + extends HandshakerServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoMethodDescriptorSupplier { + private final java.lang.String methodName; + + HandshakerServiceMethodDescriptorSupplier(java.lang.String methodName) { + this.methodName = methodName; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() { + return getServiceDescriptor().findMethodByName(methodName); + } + } + + private static volatile io.grpc.ServiceDescriptor serviceDescriptor; + + public static io.grpc.ServiceDescriptor getServiceDescriptor() { + io.grpc.ServiceDescriptor result = serviceDescriptor; + if (result == null) { + synchronized (HandshakerServiceGrpc.class) { + result = serviceDescriptor; + if (result == null) { + serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME) + .setSchemaDescriptor(new HandshakerServiceFileDescriptorSupplier()) + .addMethod(getDoHandshakeMethod()) + .build(); + } + } + } + return result; + } +} diff --git a/alts/src/main/java/io/grpc/alts/AltsChannelBuilder.java b/alts/src/main/java/io/grpc/alts/AltsChannelBuilder.java new file mode 100644 index 00000000000..ca33f8b00b9 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AltsChannelBuilder.java @@ -0,0 +1,97 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.ExperimentalApi; +import io.grpc.ForwardingChannelBuilder2; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.internal.GrpcUtil; +import io.grpc.netty.InternalNettyChannelBuilder; +import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; +import io.grpc.netty.NettyChannelBuilder; +import javax.annotation.Nullable; + +/** + * ALTS version of {@code ManagedChannelBuilder}. This class sets up a secure and authenticated + * communication between two cloud VMs using ALTS. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") +public final class AltsChannelBuilder extends ForwardingChannelBuilder2 { + private final NettyChannelBuilder delegate; + private final AltsChannelCredentials.Builder credentialsBuilder = + new AltsChannelCredentials.Builder(); + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static AltsChannelBuilder forTarget(String target) { + return new AltsChannelBuilder(target); + } + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static AltsChannelBuilder forAddress(String name, int port) { + return forTarget(GrpcUtil.authorityFromHostAndPort(name, port)); + } + + private AltsChannelBuilder(String target) { + delegate = NettyChannelBuilder.forTarget(target); + } + + /** + * Adds an expected target service accounts. One of the added service accounts should match peer + * service account in the handshaker result. Otherwise, the handshake fails. + */ + public AltsChannelBuilder addTargetServiceAccount(String targetServiceAccount) { + credentialsBuilder.addTargetServiceAccount(targetServiceAccount); + return this; + } + + /** + * Enables untrusted ALTS for testing. If this function is called, we will not check whether ALTS + * is running on Google Cloud Platform. + */ + public AltsChannelBuilder enableUntrustedAltsForTesting() { + credentialsBuilder.enableUntrustedAltsForTesting(); + return this; + } + + /** Sets a new handshaker service address for testing. */ + public AltsChannelBuilder setHandshakerAddressForTesting(String handshakerAddress) { + credentialsBuilder.setHandshakerAddressForTesting(handshakerAddress); + return this; + } + + @Override + protected NettyChannelBuilder delegate() { + return delegate; + } + + @Override + public ManagedChannel build() { + InternalNettyChannelBuilder.setProtocolNegotiatorFactory( + delegate(), + credentialsBuilder.buildProtocolNegotiatorFactory()); + + return delegate().build(); + } + + @VisibleForTesting + @Nullable + ProtocolNegotiator getProtocolNegotiatorForTest() { + return credentialsBuilder.buildProtocolNegotiatorFactory().newNegotiator(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/AltsChannelCredentials.java b/alts/src/main/java/io/grpc/alts/AltsChannelCredentials.java new file mode 100644 index 00000000000..e12344f73d7 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AltsChannelCredentials.java @@ -0,0 +1,159 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import com.google.common.collect.ImmutableList; +import io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.ExperimentalApi; +import io.grpc.Status; +import io.grpc.alts.internal.AltsProtocolNegotiator.ClientAltsProtocolNegotiatorFactory; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.InternalNettyChannelCredentials; +import io.grpc.netty.InternalProtocolNegotiator; +import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AsciiString; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides secure and authenticated communication between two cloud VMs using ALTS. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") +public final class AltsChannelCredentials { + private static final Logger logger = Logger.getLogger(AltsChannelCredentials.class.getName()); + + private AltsChannelCredentials() {} + + public static ChannelCredentials create() { + return newBuilder().build(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") + public static final class Builder { + private final ImmutableList.Builder targetServiceAccountsBuilder = + ImmutableList.builder(); + private ObjectPool handshakerChannelPool = + SharedResourcePool.forResource(HandshakerServiceChannel.SHARED_HANDSHAKER_CHANNEL); + private boolean enableUntrustedAlts; + + /** + * Adds an expected target service accounts. One of the added service accounts should match peer + * service account in the handshaker result. Otherwise, the handshake fails. + */ + public Builder addTargetServiceAccount(String targetServiceAccount) { + targetServiceAccountsBuilder.add(targetServiceAccount); + return this; + } + + /** + * Enables untrusted ALTS for testing. If this function is called, we will not check whether + * ALTS is running on Google Cloud Platform. + */ + public Builder enableUntrustedAltsForTesting() { + enableUntrustedAlts = true; + return this; + } + + /** Sets a new handshaker service address for testing. */ + public Builder setHandshakerAddressForTesting(String handshakerAddress) { + // Instead of using the default shared channel to the handshaker service, create a separate + // resource to the test address. + handshakerChannelPool = + SharedResourcePool.forResource( + HandshakerServiceChannel.getHandshakerChannelForTesting(handshakerAddress)); + return this; + } + + public ChannelCredentials build() { + return InternalNettyChannelCredentials.create(buildProtocolNegotiatorFactory()); + } + + InternalProtocolNegotiator.ClientFactory buildProtocolNegotiatorFactory() { + if (!InternalCheckGcpEnvironment.isOnGcp()) { + if (enableUntrustedAlts) { + logger.log( + Level.WARNING, + "Untrusted ALTS mode is enabled and we cannot guarantee the trustworthiness of the " + + "ALTS handshaker service"); + } else { + Status status = Status.INTERNAL.withDescription( + "ALTS is only allowed to run on Google Cloud Platform"); + return new FailingProtocolNegotiatorFactory(status); + } + } + + return new ClientAltsProtocolNegotiatorFactory( + targetServiceAccountsBuilder.build(), handshakerChannelPool); + } + } + + private static final class FailingProtocolNegotiatorFactory + implements InternalProtocolNegotiator.ClientFactory { + private final Status status; + + public FailingProtocolNegotiatorFactory(Status status) { + this.status = status; + } + + @Override + public ProtocolNegotiator newNegotiator() { + return new FailingProtocolNegotiator(status); + } + + @Override + public int getDefaultPort() { + return 443; + } + } + + private static final AsciiString SCHEME = AsciiString.of("https"); + + static final class FailingProtocolNegotiator implements ProtocolNegotiator { + private final Status status; + + public FailingProtocolNegotiator(Status status) { + this.status = status; + } + + @Override + public AsciiString scheme() { + return SCHEME; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + return new ChannelHandlerAdapter() { + @Override public void handlerAdded(ChannelHandlerContext ctx) { + ctx.fireExceptionCaught(status.asRuntimeException()); + } + }; + } + + @Override + public void close() {} + } +} diff --git a/alts/src/main/java/io/grpc/alts/AltsContext.java b/alts/src/main/java/io/grpc/alts/AltsContext.java new file mode 100644 index 00000000000..7680de4160e --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AltsContext.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.ExperimentalApi; +import io.grpc.alts.internal.AltsInternalContext; +import io.grpc.alts.internal.HandshakerResult; +import io.grpc.alts.internal.Identity; + +/** {@code AltsContext} contains security-related information on the ALTS channel. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7864") +public final class AltsContext { + + private final AltsInternalContext wrapped; + + AltsContext(AltsInternalContext wrapped) { + this.wrapped = wrapped; + } + + /** + * Creates an {@code AltsContext} for testing purposes. + * @param peerServiceAccount the peer service account of the to be created {@code AltsContext} + * @param localServiceAccount the local service account of the to be created {@code AltsContext} + * @return the created {@code AltsContext} + */ + public static AltsContext createTestInstance(String peerServiceAccount, + String localServiceAccount) { + return new AltsContext(new AltsInternalContext(HandshakerResult.newBuilder() + .setPeerIdentity(Identity.newBuilder().setServiceAccount(peerServiceAccount).build()) + .setLocalIdentity(Identity.newBuilder().setServiceAccount(localServiceAccount).build()) + .build())); + } + + /** + * Get security level. + * + * @return the context's security level. + */ + public SecurityLevel getSecurityLevel() { + switch (wrapped.getSecurityLevel()) { + case SECURITY_NONE: + return SecurityLevel.SECURITY_NONE; + case INTEGRITY_ONLY: + return SecurityLevel.INTEGRITY_ONLY; + case INTEGRITY_AND_PRIVACY: + return SecurityLevel.INTEGRITY_AND_PRIVACY; + default: + return SecurityLevel.UNKNOWN; + } + } + + /** + * Get peer service account. + * + * @return the context's peer service account. + */ + public String getPeerServiceAccount() { + return wrapped.getPeerServiceAccount(); + } + + /** + * Get local service account. + * + * @return the context's local service account. + */ + public String getLocalServiceAccount() { + return wrapped.getLocalServiceAccount(); + } + + /** SecurityLevel of the ALTS channel. */ + public enum SecurityLevel { + UNKNOWN, + SECURITY_NONE, + INTEGRITY_ONLY, + INTEGRITY_AND_PRIVACY, + } +} diff --git a/alts/src/main/java/io/grpc/alts/AltsContextUtil.java b/alts/src/main/java/io/grpc/alts/AltsContextUtil.java new file mode 100644 index 00000000000..f45179bbd91 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AltsContextUtil.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.Attributes; +import io.grpc.ClientCall; +import io.grpc.ExperimentalApi; +import io.grpc.ServerCall; +import io.grpc.alts.internal.AltsInternalContext; +import io.grpc.alts.internal.AltsProtocolNegotiator; + +/** Utility class for {@link AltsContext}. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7864") +public final class AltsContextUtil { + + private AltsContextUtil() {} + + /** + * Creates an {@link AltsContext} from ALTS context information in the {@link ServerCall}. + * + * @param call the {@link ServerCall} containing the ALTS information + * @return the created {@link AltsContext} + * @throws IllegalArgumentException if the {@link ServerCall} has no ALTS information. + */ + public static AltsContext createFrom(ServerCall call) { + return createFrom(call.getAttributes()); + } + + /** + * Creates an {@link AltsContext} from ALTS context information in the {@link ClientCall}. + * + * @param call the {@link ClientCall} containing the ALTS information + * @return the created {@link AltsContext} + * @throws IllegalArgumentException if the {@link ClientCall} has no ALTS information. + */ + public static AltsContext createFrom(ClientCall call) { + return createFrom(call.getAttributes()); + } + + /** + * Creates an {@link AltsContext} from ALTS context information in the {@link Attributes}. + * + * @param attributes the {@link Attributes} containing the ALTS information + * @return the created {@link AltsContext} + * @throws IllegalArgumentException if the {@link Attributes} has no ALTS information. + */ + public static AltsContext createFrom(Attributes attributes) { + Object authContext = attributes.get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); + if (!(authContext instanceof AltsInternalContext)) { + throw new IllegalArgumentException("No ALTS context information found"); + } + return new AltsContext((AltsInternalContext) authContext); + } + + /** + * Checks if the {@link ServerCall} contains ALTS information. + * + * @param call the {@link ServerCall} to check + * @return true, if the {@link ServerCall} contains ALTS information and false otherwise. + */ + public static boolean check(ServerCall call) { + return check(call.getAttributes()); + } + + /** + * Checks if the {@link ClientCall} contains ALTS information. + * + * @param call the {@link ClientCall} to check + * @return true, if the {@link ClientCall} contains ALTS information and false otherwise. + */ + public static boolean check(ClientCall call) { + return check(call.getAttributes()); + } + + /** + * Checks if the {@link Attributes} contains ALTS information. + * + * @param attributes the {@link Attributes} to check + * @return true, if the {@link Attributes} contains ALTS information and false otherwise. + */ + public static boolean check(Attributes attributes) { + Object authContext = attributes.get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); + return authContext instanceof AltsInternalContext; + } +} diff --git a/alts/src/main/java/io/grpc/alts/AltsServerBuilder.java b/alts/src/main/java/io/grpc/alts/AltsServerBuilder.java new file mode 100644 index 00000000000..e307fd1c63a --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AltsServerBuilder.java @@ -0,0 +1,166 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.BindableService; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ExperimentalApi; +import io.grpc.ForwardingServerBuilder; +import io.grpc.HandlerRegistry; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServerStreamTracer; +import io.grpc.ServerTransportFilter; +import io.grpc.netty.NettyServerBuilder; +import java.io.File; +import java.net.InetSocketAddress; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * gRPC secure server builder used for ALTS. This class adds on the necessary ALTS support to create + * a production server on Google Cloud Platform. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") +public final class AltsServerBuilder extends ForwardingServerBuilder { + private final NettyServerBuilder delegate; + private final AltsServerCredentials.Builder credentialsBuilder = + new AltsServerCredentials.Builder(); + + private AltsServerBuilder(NettyServerBuilder nettyDelegate) { + this.delegate = nettyDelegate; + } + + /** Creates a gRPC server builder for the given port. */ + public static AltsServerBuilder forPort(int port) { + NettyServerBuilder nettyDelegate = NettyServerBuilder.forAddress(new InetSocketAddress(port)); + return new AltsServerBuilder(nettyDelegate); + } + + /** + * Enables untrusted ALTS for testing. If this function is called, we will not check whether ALTS + * is running on Google Cloud Platform. + */ + public AltsServerBuilder enableUntrustedAltsForTesting() { + credentialsBuilder.enableUntrustedAltsForTesting(); + return this; + } + + /** Sets a new handshaker service address for testing. */ + public AltsServerBuilder setHandshakerAddressForTesting(String handshakerAddress) { + credentialsBuilder.setHandshakerAddressForTesting(handshakerAddress); + return this; + } + + @Override + protected ServerBuilder delegate() { + return delegate; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder handshakeTimeout(long timeout, TimeUnit unit) { + delegate.handshakeTimeout(timeout, unit); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder directExecutor() { + delegate.directExecutor(); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder addStreamTracerFactory(ServerStreamTracer.Factory factory) { + delegate.addStreamTracerFactory(factory); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder addTransportFilter(ServerTransportFilter filter) { + delegate.addTransportFilter(filter); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder executor(Executor executor) { + delegate.executor(executor); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder addService(ServerServiceDefinition service) { + delegate.addService(service); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder addService(BindableService bindableService) { + delegate.addService(bindableService); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder fallbackHandlerRegistry(HandlerRegistry fallbackRegistry) { + delegate.fallbackHandlerRegistry(fallbackRegistry); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder useTransportSecurity(File certChain, File privateKey) { + throw new UnsupportedOperationException("Can't set TLS settings for ALTS"); + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder decompressorRegistry(DecompressorRegistry registry) { + delegate.decompressorRegistry(registry); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder compressorRegistry(CompressorRegistry registry) { + delegate.compressorRegistry(registry); + return this; + } + + /** {@inheritDoc} */ + @Override + public AltsServerBuilder intercept(ServerInterceptor interceptor) { + delegate.intercept(interceptor); + return this; + } + + /** {@inheritDoc} */ + @Override + public Server build() { + delegate.protocolNegotiator(credentialsBuilder.buildProtocolNegotiator()); + return delegate.build(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/AltsServerCredentials.java b/alts/src/main/java/io/grpc/alts/AltsServerCredentials.java new file mode 100644 index 00000000000..36dc9f6a4ae --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AltsServerCredentials.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.Channel; +import io.grpc.ExperimentalApi; +import io.grpc.ServerCredentials; +import io.grpc.Status; +import io.grpc.alts.internal.AltsProtocolNegotiator; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; +import io.grpc.netty.InternalNettyServerCredentials; +import io.grpc.netty.InternalProtocolNegotiator; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * gRPC secure server builder used for ALTS. This class adds on the necessary ALTS support to create + * a production server on Google Cloud Platform. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") +public final class AltsServerCredentials { + private static final Logger logger = Logger.getLogger(AltsServerCredentials.class.getName()); + + private AltsServerCredentials() {} + + public static ServerCredentials create() { + return newBuilder().build(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") + public static final class Builder { + private ObjectPool handshakerChannelPool = + SharedResourcePool.forResource(HandshakerServiceChannel.SHARED_HANDSHAKER_CHANNEL); + private boolean enableUntrustedAlts; + + /** + * Enables untrusted ALTS for testing. If this function is called, we will not check whether + * ALTS is running on Google Cloud Platform. + */ + public Builder enableUntrustedAltsForTesting() { + enableUntrustedAlts = true; + return this; + } + + /** Sets a new handshaker service address for testing. */ + public Builder setHandshakerAddressForTesting(String handshakerAddress) { + // Instead of using the default shared channel to the handshaker service, create a separate + // resource to the test address. + handshakerChannelPool = + SharedResourcePool.forResource( + HandshakerServiceChannel.getHandshakerChannelForTesting(handshakerAddress)); + return this; + } + + public ServerCredentials build() { + return InternalNettyServerCredentials.create(buildProtocolNegotiator()); + } + + InternalProtocolNegotiator.ProtocolNegotiator buildProtocolNegotiator() { + if (!InternalCheckGcpEnvironment.isOnGcp()) { + if (enableUntrustedAlts) { + logger.log( + Level.WARNING, + "Untrusted ALTS mode is enabled and we cannot guarantee the trustworthiness of the " + + "ALTS handshaker service"); + } else { + Status status = Status.INTERNAL.withDescription( + "ALTS is only allowed to run on Google Cloud Platform"); + return new AltsChannelCredentials.FailingProtocolNegotiator(status); + } + } + + return AltsProtocolNegotiator.serverAltsProtocolNegotiator(handshakerChannelPool); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/AuthorizationUtil.java b/alts/src/main/java/io/grpc/alts/AuthorizationUtil.java new file mode 100644 index 00000000000..53e45105d9b --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AuthorizationUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.ServerCall; +import io.grpc.Status; +import io.grpc.alts.internal.AltsInternalContext; +import io.grpc.alts.internal.AltsProtocolNegotiator; +import java.util.Collection; + +/** Utility class for ALTS client authorization. */ +public final class AuthorizationUtil { + + private AuthorizationUtil() {} + + /** + * Given a server call, performs client authorization check, i.e., checks if the client service + * account matches one of the expected service accounts. It returns OK if client is authorized and + * an error otherwise. + */ + public static Status clientAuthorizationCheck( + ServerCall call, Collection expectedServiceAccounts) { + AltsInternalContext altsContext = + (AltsInternalContext) call.getAttributes().get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); + if (altsContext == null) { + return Status.PERMISSION_DENIED.withDescription("Peer ALTS AuthContext not found"); + } + if (expectedServiceAccounts.contains(altsContext.getPeerServiceAccount())) { + return Status.OK; + } + return Status.PERMISSION_DENIED.withDescription( + "Client " + altsContext.getPeerServiceAccount() + " is not authorized"); + } +} diff --git a/alts/src/main/java/io/grpc/alts/ComputeEngineChannelBuilder.java b/alts/src/main/java/io/grpc/alts/ComputeEngineChannelBuilder.java new file mode 100644 index 00000000000..b5ee6a8d362 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/ComputeEngineChannelBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.ForwardingChannelBuilder; +import io.grpc.ManagedChannelBuilder; +import io.grpc.internal.GrpcUtil; +import io.grpc.netty.NettyChannelBuilder; + +/** + * {@code ManagedChannelBuilder} for Google Compute Engine. This class sets up a secure channel + * using ALTS if applicable and using TLS as fallback. + */ +public final class ComputeEngineChannelBuilder + extends ForwardingChannelBuilder { + + private final NettyChannelBuilder delegate; + + private ComputeEngineChannelBuilder(String target) { + delegate = NettyChannelBuilder.forTarget(target, ComputeEngineChannelCredentials.create()); + } + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static ComputeEngineChannelBuilder forTarget(String target) { + return new ComputeEngineChannelBuilder(target); + } + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static ComputeEngineChannelBuilder forAddress(String name, int port) { + return forTarget(GrpcUtil.authorityFromHostAndPort(name, port)); + } + + @Override + @SuppressWarnings("deprecation") // Not extending ForwardingChannelBuilder2 to preserve ABI. + protected NettyChannelBuilder delegate() { + return delegate; + } +} diff --git a/alts/src/main/java/io/grpc/alts/ComputeEngineChannelCredentials.java b/alts/src/main/java/io/grpc/alts/ComputeEngineChannelCredentials.java new file mode 100644 index 00000000000..518642a675d --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/ComputeEngineChannelCredentials.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import com.google.auth.oauth2.ComputeEngineCredentials; +import com.google.common.collect.ImmutableList; +import io.grpc.CallCredentials; +import io.grpc.ChannelCredentials; +import io.grpc.CompositeChannelCredentials; +import io.grpc.Status; +import io.grpc.alts.internal.AltsProtocolNegotiator.GoogleDefaultProtocolNegotiatorFactory; +import io.grpc.auth.MoreCallCredentials; +import io.grpc.internal.SharedResourcePool; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.InternalNettyChannelCredentials; +import io.grpc.netty.InternalProtocolNegotiator; +import io.netty.handler.ssl.SslContext; +import javax.net.ssl.SSLException; + +/** + * Credentials appropriate to contact Google services when running on Google Compute Engine. This + * class sets up a secure channel using ALTS if applicable and using TLS as fallback. It is a subset + * of the functionality provided by {@link GoogleDefaultChannelCredentials}. + */ +public final class ComputeEngineChannelCredentials { + private ComputeEngineChannelCredentials() {} + + /** + * Creates credentials for Google Compute Engine. This class sets up a secure channel using ALTS + * if applicable and using TLS as fallback. + */ + public static ChannelCredentials create() { + ChannelCredentials nettyCredentials = + InternalNettyChannelCredentials.create(createClientFactory()); + CallCredentials callCredentials; + if (InternalCheckGcpEnvironment.isOnGcp()) { + callCredentials = MoreCallCredentials.from(ComputeEngineCredentials.create()); + } else { + callCredentials = new FailingCallCredentials( + Status.INTERNAL.withDescription( + "Compute Engine Credentials can only be used on Google Cloud Platform")); + } + return CompositeChannelCredentials.create(nettyCredentials, callCredentials); + } + + private static InternalProtocolNegotiator.ClientFactory createClientFactory() { + SslContext sslContext; + try { + sslContext = GrpcSslContexts.forClient().build(); + } catch (SSLException e) { + throw new RuntimeException(e); + } + return new GoogleDefaultProtocolNegotiatorFactory( + /* targetServiceAccounts= */ ImmutableList.of(), + SharedResourcePool.forResource(HandshakerServiceChannel.SHARED_HANDSHAKER_CHANNEL), + sslContext); + } +} diff --git a/alts/src/main/java/io/grpc/alts/DualCallCredentials.java b/alts/src/main/java/io/grpc/alts/DualCallCredentials.java new file mode 100644 index 00000000000..08104712e65 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/DualCallCredentials.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.CallCredentials; +import java.util.concurrent.Executor; + +/** + * {@code CallCredentials} that will pick the right credentials based on whether the established + * connection is ALTS or TLS. + */ +final class DualCallCredentials extends CallCredentials { + private final CallCredentials tlsCallCredentials; + private final CallCredentials altsCallCredentials; + + public DualCallCredentials(CallCredentials tlsCallCreds, CallCredentials altsCallCreds) { + tlsCallCredentials = tlsCallCreds; + altsCallCredentials = altsCallCreds; + } + + @Override + public void applyRequestMetadata( + CallCredentials.RequestInfo requestInfo, + Executor appExecutor, + CallCredentials.MetadataApplier applier) { + if (AltsContextUtil.check(requestInfo.getTransportAttrs())) { + altsCallCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + } else { + tlsCallCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/FailingCallCredentials.java b/alts/src/main/java/io/grpc/alts/FailingCallCredentials.java new file mode 100644 index 00000000000..3c59c5b6d09 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/FailingCallCredentials.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import com.google.common.base.Preconditions; +import io.grpc.CallCredentials; +import io.grpc.Status; +import java.util.concurrent.Executor; + +/** + * {@code CallCredentials} that always fail the RPC. + */ +final class FailingCallCredentials extends CallCredentials { + private final Status status; + + public FailingCallCredentials(Status status) { + this.status = Preconditions.checkNotNull(status, "status"); + } + + @Override + public void applyRequestMetadata( + CallCredentials.RequestInfo requestInfo, + Executor appExecutor, + CallCredentials.MetadataApplier applier) { + applier.fail(status); + } +} diff --git a/alts/src/main/java/io/grpc/alts/FailingClientCall.java b/alts/src/main/java/io/grpc/alts/FailingClientCall.java new file mode 100644 index 00000000000..ab674364ee6 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/FailingClientCall.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.Status; + +/** An implementation of {@link ClientCall} that fails when started. */ +final class FailingClientCall extends ClientCall { + + private final Status error; + + public FailingClientCall(Status error) { + this.error = error; + } + + @Override + public void start(ClientCall.Listener listener, Metadata headers) { + listener.onClose(error, new Metadata()); + } + + @Override + public void request(int numMessages) {} + + @Override + public void cancel(String message, Throwable cause) {} + + @Override + public void halfClose() {} + + @Override + public void sendMessage(ReqT message) {} +} diff --git a/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelBuilder.java b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelBuilder.java new file mode 100644 index 00000000000..c78b94417c4 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.ForwardingChannelBuilder; +import io.grpc.ManagedChannelBuilder; +import io.grpc.internal.GrpcUtil; +import io.grpc.netty.NettyChannelBuilder; + +/** + * Google default version of {@code ManagedChannelBuilder}. This class sets up a secure channel + * using ALTS if applicable and using TLS as fallback. + */ +public final class GoogleDefaultChannelBuilder + extends ForwardingChannelBuilder { + + private final NettyChannelBuilder delegate; + + private GoogleDefaultChannelBuilder(String target) { + delegate = NettyChannelBuilder.forTarget(target, GoogleDefaultChannelCredentials.create()); + } + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static GoogleDefaultChannelBuilder forTarget(String target) { + return new GoogleDefaultChannelBuilder(target); + } + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static GoogleDefaultChannelBuilder forAddress(String name, int port) { + return forTarget(GrpcUtil.authorityFromHostAndPort(name, port)); + } + + @Override + @SuppressWarnings("deprecation") // Not extending ForwardingChannelBuilder2 to preserve ABI. + protected NettyChannelBuilder delegate() { + return delegate; + } +} diff --git a/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelCredentials.java b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelCredentials.java new file mode 100644 index 00000000000..1b5880120a4 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelCredentials.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableList; +import io.grpc.CallCredentials; +import io.grpc.ChannelCredentials; +import io.grpc.CompositeChannelCredentials; +import io.grpc.Status; +import io.grpc.alts.internal.AltsProtocolNegotiator.GoogleDefaultProtocolNegotiatorFactory; +import io.grpc.auth.MoreCallCredentials; +import io.grpc.internal.SharedResourcePool; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.InternalNettyChannelCredentials; +import io.grpc.netty.InternalProtocolNegotiator; +import io.netty.handler.ssl.SslContext; +import java.io.IOException; +import javax.net.ssl.SSLException; + +/** + * Credentials appropriate to contact Google services. This class sets up a secure channel using + * ALTS if applicable and uses TLS as fallback. + */ +public final class GoogleDefaultChannelCredentials { + private GoogleDefaultChannelCredentials() {} + + /** + * Creates Google default credentials uses a secure channel with ALTS if applicable and uses TLS + * as fallback. + */ + public static ChannelCredentials create() { + return newBuilder().build(); + } + + /** + * Returns a new instance of {@link Builder}. + * + * @since 1.43.0 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Builder for {@link GoogleDefaultChannelCredentials} instances. + * + * @since 1.43.0 + */ + public static final class Builder { + private CallCredentials callCredentials; + private CallCredentials altsCallCredentials; + + private Builder() {} + + /** Constructs GoogleDefaultChannelCredentials with a given call credential. */ + public Builder callCredentials(CallCredentials callCreds) { + callCredentials = callCreds; + return this; + } + + /** Constructs GoogleDefaultChannelCredentials with an ALTS-specific call credential. */ + public Builder altsCallCredentials(CallCredentials callCreds) { + altsCallCredentials = callCreds; + return this; + } + + /** Builds a GoogleDefaultChannelCredentials instance. */ + public ChannelCredentials build() { + ChannelCredentials nettyCredentials = + InternalNettyChannelCredentials.create(createClientFactory()); + CallCredentials tlsCallCreds = callCredentials; + if (tlsCallCreds == null) { + try { + tlsCallCreds = MoreCallCredentials.from(GoogleCredentials.getApplicationDefault()); + } catch (IOException e) { + tlsCallCreds = + new FailingCallCredentials( + Status.UNAUTHENTICATED + .withDescription("Failed to get Google default credentials") + .withCause(e)); + } + } + CallCredentials callCreds = + altsCallCredentials == null + ? tlsCallCreds + : new DualCallCredentials(tlsCallCreds, altsCallCredentials); + return CompositeChannelCredentials.create(nettyCredentials, callCreds); + } + + private static InternalProtocolNegotiator.ClientFactory createClientFactory() { + SslContext sslContext; + try { + sslContext = GrpcSslContexts.forClient().build(); + } catch (SSLException e) { + throw new RuntimeException(e); + } + return new GoogleDefaultProtocolNegotiatorFactory( + /* targetServiceAccounts= */ ImmutableList.of(), + SharedResourcePool.forResource(HandshakerServiceChannel.SHARED_HANDSHAKER_CHANNEL), + sslContext); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java new file mode 100644 index 00000000000..5e32d22d901 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java @@ -0,0 +1,151 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.SharedResourceHolder.Resource; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +/** + * Class for creating a single shared gRPC channel to the ALTS Handshaker Service using + * SharedResourceHolder. The channel to the handshaker service is local and is over plaintext. Each + * application will have at most one connection to the handshaker service. + */ +final class HandshakerServiceChannel { + // Port 8080 is necessary for ALTS handshake. + private static final int ALTS_PORT = 8080; + private static final String DEFAULT_TARGET = "metadata.google.internal.:8080"; + + static final Resource SHARED_HANDSHAKER_CHANNEL = + new ChannelResource(getHandshakerTarget(System.getenv("GCE_METADATA_HOST"))); + + /** + * Returns handshaker target. When GCE_METADATA_HOST is provided, it might contain port which we + * will discard and use ALTS_PORT instead. + */ + static String getHandshakerTarget(String envValue) { + if (envValue == null || envValue.isEmpty()) { + return DEFAULT_TARGET; + } + String host = envValue; + int portIndex = host.lastIndexOf(':'); + if (portIndex != -1) { + host = host.substring(0, portIndex); // Discard port if specified + } + return host + ":" + ALTS_PORT; // Utilize ALTS port in all cases + } + + /** Returns a resource of handshaker service channel for testing only. */ + static Resource getHandshakerChannelForTesting(String handshakerAddress) { + return new ChannelResource(handshakerAddress); + } + + private static final boolean EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS", false); + + private static class ChannelResource implements Resource { + private final String target; + + public ChannelResource(String target) { + this.target = target; + } + + @Override + public Channel create() { + /* Use its own event loop thread pool to avoid blocking. */ + EventLoopGroup eventGroup = + new NioEventLoopGroup(1, new DefaultThreadFactory("handshaker pool", true)); + NettyChannelBuilder channelBuilder = + NettyChannelBuilder.forTarget(target) + .channelType(NioSocketChannel.class, InetSocketAddress.class) + .directExecutor() + .eventLoopGroup(eventGroup) + .usePlaintext(); + if (EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS) { + channelBuilder.keepAliveTime(10, TimeUnit.MINUTES).keepAliveTimeout(10, TimeUnit.SECONDS); + } + ManagedChannel channel = channelBuilder.build(); + return new EventLoopHoldingChannel(channel, eventGroup); + } + + @Override + public void close(Channel instanceChannel) { + ((EventLoopHoldingChannel) instanceChannel).close(); + } + + @Override + public String toString() { + return "grpc-alts-handshaker-service-channel"; + } + } + + private abstract static class ForwardingChannel extends Channel { + protected abstract Channel delegate(); + + @Override + public String authority() { + return delegate().authority(); + } + + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, CallOptions options) { + return delegate().newCall(methodDescriptor, options); + } + } + + private static class EventLoopHoldingChannel extends ForwardingChannel { + private final ManagedChannel delegate; + private final EventLoopGroup eventLoopGroup; + + public EventLoopHoldingChannel(ManagedChannel delegate, EventLoopGroup eventLoopGroup) { + this.delegate = delegate; + this.eventLoopGroup = eventLoopGroup; + } + + @Override + protected Channel delegate() { + return delegate; + } + + @SuppressWarnings("FutureReturnValueIgnored") // netty ChannelFuture + public void close() { + // This method will generally be run on the ResourceHolder's ScheduledExecutorService thread + delegate.shutdownNow(); + boolean terminated = false; + try { + terminated = delegate.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + // terminated will be false + } + // Try hard to shutdown abruptly so any bug is more likely to be noticed during testing. + long quietPeriodSeconds = terminated ? 0 : 1; + eventLoopGroup.shutdownGracefully(quietPeriodSeconds, 10, TimeUnit.SECONDS); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/InternalCheckGcpEnvironment.java b/alts/src/main/java/io/grpc/alts/InternalCheckGcpEnvironment.java new file mode 100644 index 00000000000..aae4a45d530 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/InternalCheckGcpEnvironment.java @@ -0,0 +1,97 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.Internal; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class for checking if the system is running on Google Cloud Platform (GCP). This is intended for + * usage internal to the gRPC team. If you *really* think you need to use this, contact the gRPC + * team first. + */ +@Internal +public final class InternalCheckGcpEnvironment { + + private static final Logger logger = + Logger.getLogger(InternalCheckGcpEnvironment.class.getName()); + private static final String WINDOWS_COMMAND = "powershell.exe"; + private static Boolean cachedResult = null; + + // Construct me not! + private InternalCheckGcpEnvironment() {} + + /** Returns {@code true} if currently running on Google Cloud Platform (GCP). */ + public static synchronized boolean isOnGcp() { + if (cachedResult == null) { + cachedResult = isRunningOnGcp(); + } + return cachedResult; + } + + @VisibleForTesting + static boolean checkProductNameOnLinux(BufferedReader reader) throws IOException { + String name = reader.readLine().trim(); + return name.equals("Google") || name.equals("Google Compute Engine"); + } + + @VisibleForTesting + static boolean checkBiosDataOnWindows(BufferedReader reader) throws IOException { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("Manufacturer")) { + String name = line.substring(line.indexOf(':') + 1).trim(); + return name.equals("Google"); + } + } + return false; + } + + private static boolean isRunningOnGcp() { + String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); + try { + if (osName.startsWith("linux")) { + // Checks GCE residency on Linux platform. + return checkProductNameOnLinux( + Files.newBufferedReader(Paths.get("/sys/class/dmi/id/product_name"), UTF_8)); + } else if (osName.startsWith("windows")) { + // Checks GCE residency on Windows platform. + Process p = + new ProcessBuilder() + .command(WINDOWS_COMMAND, "Get-WmiObject", "-Class", "Win32_BIOS") + .start(); + return checkBiosDataOnWindows( + new BufferedReader(new InputStreamReader(p.getInputStream(), UTF_8))); + } + } catch (IOException e) { + logger.log(Level.WARNING, "Fail to read platform information: ", e); + return false; + } + // Platforms other than Linux and Windows are not supported. + return false; + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AeadCrypter.java b/alts/src/main/java/io/grpc/alts/internal/AeadCrypter.java new file mode 100644 index 00000000000..4d99187c33d --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AeadCrypter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** + * {@code AeadCrypter} performs authenticated encryption and decryption for a fixed key given unique + * nonces. Authenticated additional data is supported. + */ +interface AeadCrypter { + /** + * Encrypt plaintext into ciphertext buffer using the given nonce. + * + * @param ciphertext the encrypted plaintext and the tag will be written into this buffer. + * @param plaintext the input that should be encrypted. + * @param nonce the unique nonce used for the encryption. + * @throws GeneralSecurityException if ciphertext buffer is short or the nonce does not have the + * expected size. + */ + void encrypt(ByteBuffer ciphertext, ByteBuffer plaintext, byte[] nonce) + throws GeneralSecurityException; + + /** + * Encrypt plaintext into ciphertext buffer using the given nonce with authenticated data. + * + * @param ciphertext the encrypted plaintext and the tag will be written into this buffer. + * @param plaintext the input that should be encrypted. + * @param aad additional data that should be authenticated, but not encrypted. + * @param nonce the unique nonce used for the encryption. + * @throws GeneralSecurityException if ciphertext buffer is short or the nonce does not have the + * expected size. + */ + void encrypt(ByteBuffer ciphertext, ByteBuffer plaintext, ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException; + + /** + * Decrypt ciphertext into plaintext buffer using the given nonce. + * + * @param plaintext the decrypted plaintext will be written into this buffer. + * @param ciphertext the ciphertext and tag that should be decrypted. + * @param nonce the nonce that was used for the encryption. + * @throws GeneralSecurityException if the tag is invalid or any of the inputs do not have the + * expected size. + */ + void decrypt(ByteBuffer plaintext, ByteBuffer ciphertext, byte[] nonce) + throws GeneralSecurityException; + + /** + * Decrypt ciphertext into plaintext buffer using the given nonce. + * + * @param plaintext the decrypted plaintext will be written into this buffer. + * @param ciphertext the ciphertext and tag that should be decrypted. + * @param aad additional data that is checked for authenticity. + * @param nonce the nonce that was used for the encryption. + * @throws GeneralSecurityException if the tag is invalid or any of the inputs do not have the + * expected size. + */ + void decrypt(ByteBuffer plaintext, ByteBuffer ciphertext, ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException; +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AesGcmAeadCrypter.java b/alts/src/main/java/io/grpc/alts/internal/AesGcmAeadCrypter.java new file mode 100644 index 00000000000..e3e6302b591 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AesGcmAeadCrypter.java @@ -0,0 +1,158 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.internal.ConscryptLoader; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Provider; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** AES128-GCM implementation of {@link AeadCrypter} that uses default JCE provider. */ +final class AesGcmAeadCrypter implements AeadCrypter { + private static final Logger logger = Logger.getLogger(AesGcmAeadCrypter.class.getName()); + private static final int KEY_LENGTH = 16; + private static final int TAG_LENGTH = 16; + static final int NONCE_LENGTH = 12; + + private static final String AES = "AES"; + private static final String AES_GCM = AES + "/GCM/NoPadding"; + // Conscrypt if available, otherwise null. Conscrypt is much faster than Java 8's JSSE + private static final Provider CONSCRYPT = getConscrypt(); + + private final byte[] key; + private final Cipher cipher; + + AesGcmAeadCrypter(byte[] key) throws GeneralSecurityException { + checkArgument(key.length == KEY_LENGTH); + this.key = key; + if (CONSCRYPT != null) { + cipher = Cipher.getInstance(AES_GCM, CONSCRYPT); + } else { + cipher = Cipher.getInstance(AES_GCM); + } + } + + private int encryptAad( + ByteBuffer ciphertext, ByteBuffer plaintext, @Nullable ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException { + checkArgument(nonce.length == NONCE_LENGTH); + cipher.init( + Cipher.ENCRYPT_MODE, + new SecretKeySpec(this.key, AES), + new GCMParameterSpec(TAG_LENGTH * 8, nonce)); + if (aad != null) { + cipher.updateAAD(aad); + } + return cipher.doFinal(plaintext, ciphertext); + } + + private void decryptAad( + ByteBuffer plaintext, ByteBuffer ciphertext, @Nullable ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException { + checkArgument(nonce.length == NONCE_LENGTH); + cipher.init( + Cipher.DECRYPT_MODE, + new SecretKeySpec(this.key, AES), + new GCMParameterSpec(TAG_LENGTH * 8, nonce)); + if (aad != null) { + cipher.updateAAD(aad); + } + cipher.doFinal(ciphertext, plaintext); + } + + @Override + public void encrypt(ByteBuffer ciphertext, ByteBuffer plaintext, byte[] nonce) + throws GeneralSecurityException { + encryptAad(ciphertext, plaintext, null, nonce); + } + + @Override + public void encrypt(ByteBuffer ciphertext, ByteBuffer plaintext, ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException { + encryptAad(ciphertext, plaintext, aad, nonce); + } + + @Override + public void decrypt(ByteBuffer plaintext, ByteBuffer ciphertext, byte[] nonce) + throws GeneralSecurityException { + decryptAad(plaintext, ciphertext, null, nonce); + } + + @Override + public void decrypt(ByteBuffer plaintext, ByteBuffer ciphertext, ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException { + decryptAad(plaintext, ciphertext, aad, nonce); + } + + static int getKeyLength() { + return KEY_LENGTH; + } + + @VisibleForTesting + static Provider getConscrypt() { + if (!ConscryptLoader.isPresent()) { + return null; + } + // Conscrypt 2.1.0 or later is required. If an older version is used, it will fail with these + // sorts of errors: + // "The underlying Cipher implementation does not support this method" + // "error:1e000067:Cipher functions:OPENSSL_internal:BUFFER_TOO_SMALL" + // + // While we could use Conscrypt.version() to check compatibility, that is _very_ verbose via + // reflection. In practice, old conscrypts are probably not much of a problem. + Provider provider; + try { + provider = ConscryptLoader.newProvider(); + } catch (Throwable t) { + logger.log(Level.INFO, "Could not load Conscrypt. Will use slower JDK implementation", t); + return null; + } + try { + Cipher.getInstance(AES_GCM, provider); + } catch (SecurityException t) { + // Pre-Java 7u121/Java 8u111 fails with SecurityException: + // JCE cannot authenticate the provider Conscrypt + // + // This is because Conscrypt uses a newer (more secure) signing CA than the earlier Java + // supported. https://www.oracle.com/technetwork/java/javase/8u111-relnotes-3124969.html + // https://www.oracle.com/technetwork/java/javase/documentation/javase7supportreleasenotes-1601161.html#R170_121 + // + // Use WARNING instead of INFO in this case because it is unlikely to be a supported + // environment. In the other cases we might be on Java 9+; it seems unlikely in this case. + // Note that on Java 7, we're likely to crash later because GCM is unsupported. + logger.log( + Level.WARNING, + "Could not load Conscrypt. Will try slower JDK implementation. This may be because the " + + "JDK is older than Java 7 update 121 or Java 8 update 111. If so, please update", + t); + return null; + } catch (Throwable t) { + logger.log(Level.INFO, "Could not load Conscrypt. Will use slower JDK implementation", t); + return null; + } + return provider; + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AesGcmHkdfAeadCrypter.java b/alts/src/main/java/io/grpc/alts/internal/AesGcmHkdfAeadCrypter.java new file mode 100644 index 00000000000..b0f7535d35a --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AesGcmHkdfAeadCrypter.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +/** + * {@link AeadCrypter} implementation based on {@link AesGcmAeadCrypter} with nonce-based rekeying + * using HKDF-expand and random nonce-mask that is XORed with the given nonce/counter. The AES-GCM + * key is computed as HKDF-expand(kdfKey, nonce[2..7]), i.e., the first 2 bytes are ignored to + * require rekeying only after 2^16 operations and the last 4 bytes (including the direction bit) + * are ignored to allow for optimizations (use same AEAD context for both directions, store counter + * as unsigned long and boolean for direction). + */ +final class AesGcmHkdfAeadCrypter implements AeadCrypter { + private static final int KDF_KEY_LENGTH = 32; + // Rekey after 2^(2*8) = 2^16 operations by ignoring the first 2 nonce bytes for key derivation. + private static final int KDF_COUNTER_OFFSET = 2; + // Use remaining bytes of 64-bit counter included in nonce for key derivation. + private static final int KDF_COUNTER_LENGTH = 6; + private static final int NONCE_LENGTH = AesGcmAeadCrypter.NONCE_LENGTH; + private static final int KEY_LENGTH = KDF_KEY_LENGTH + NONCE_LENGTH; + + private final byte[] kdfKey; + private final byte[] kdfCounter = new byte[KDF_COUNTER_LENGTH]; + private final byte[] nonceMask; + private final byte[] nonceBuffer = new byte[NONCE_LENGTH]; + + private AeadCrypter aeadCrypter; + + AesGcmHkdfAeadCrypter(byte[] key) { + checkArgument(key.length == KEY_LENGTH); + this.kdfKey = Arrays.copyOf(key, KDF_KEY_LENGTH); + this.nonceMask = Arrays.copyOfRange(key, KDF_KEY_LENGTH, KDF_KEY_LENGTH + NONCE_LENGTH); + } + + @Override + public void encrypt(ByteBuffer ciphertext, ByteBuffer plaintext, byte[] nonce) + throws GeneralSecurityException { + maybeRekey(nonce); + maskNonce(nonceBuffer, nonceMask, nonce); + aeadCrypter.encrypt(ciphertext, plaintext, nonceBuffer); + } + + @Override + public void encrypt(ByteBuffer ciphertext, ByteBuffer plaintext, ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException { + maybeRekey(nonce); + maskNonce(nonceBuffer, nonceMask, nonce); + aeadCrypter.encrypt(ciphertext, plaintext, aad, nonceBuffer); + } + + @Override + public void decrypt(ByteBuffer plaintext, ByteBuffer ciphertext, byte[] nonce) + throws GeneralSecurityException { + maybeRekey(nonce); + maskNonce(nonceBuffer, nonceMask, nonce); + aeadCrypter.decrypt(plaintext, ciphertext, nonceBuffer); + } + + @Override + public void decrypt(ByteBuffer plaintext, ByteBuffer ciphertext, ByteBuffer aad, byte[] nonce) + throws GeneralSecurityException { + maybeRekey(nonce); + maskNonce(nonceBuffer, nonceMask, nonce); + aeadCrypter.decrypt(plaintext, ciphertext, aad, nonceBuffer); + } + + private void maybeRekey(byte[] nonce) throws GeneralSecurityException { + if (aeadCrypter != null + && arrayEqualOn(nonce, KDF_COUNTER_OFFSET, kdfCounter, 0, KDF_COUNTER_LENGTH)) { + return; + } + System.arraycopy(nonce, KDF_COUNTER_OFFSET, kdfCounter, 0, KDF_COUNTER_LENGTH); + int aeKeyLen = AesGcmAeadCrypter.getKeyLength(); + byte[] aeKey = Arrays.copyOf(hkdfExpandSha256(kdfKey, kdfCounter), aeKeyLen); + aeadCrypter = new AesGcmAeadCrypter(aeKey); + } + + private static void maskNonce(byte[] nonceBuffer, byte[] nonceMask, byte[] nonce) { + checkArgument(nonce.length == NONCE_LENGTH); + for (int i = 0; i < NONCE_LENGTH; i++) { + nonceBuffer[i] = (byte) (nonceMask[i] ^ nonce[i]); + } + } + + private static byte[] hkdfExpandSha256(byte[] key, byte[] info) throws GeneralSecurityException { + Mac mac = Mac.getInstance("HMACSHA256"); + mac.init(new SecretKeySpec(key, mac.getAlgorithm())); + mac.update(info); + mac.update((byte) 0x01); + return mac.doFinal(); + } + + private static boolean arrayEqualOn(byte[] a, int aPos, byte[] b, int bPos, int length) { + for (int i = 0; i < length; i++) { + if (a[aPos + i] != b[bPos + i]) { + return false; + } + } + return true; + } + + static int getKeyLength() { + return KEY_LENGTH; + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsChannelCrypter.java b/alts/src/main/java/io/grpc/alts/internal/AltsChannelCrypter.java new file mode 100644 index 00000000000..5e4c6fec301 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsChannelCrypter.java @@ -0,0 +1,169 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Verify.verify; + +import com.google.common.annotations.VisibleForTesting; +import io.netty.buffer.ByteBuf; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.List; + +/** Performs encryption and decryption with AES-GCM using JCE. All methods are thread-compatible. */ +final class AltsChannelCrypter implements ChannelCrypterNetty { + private static final int KEY_LENGTH = AesGcmHkdfAeadCrypter.getKeyLength(); + private static final int COUNTER_LENGTH = 12; + // The counter will overflow after 2^64 operations and encryption/decryption will stop working. + private static final int COUNTER_OVERFLOW_LENGTH = 8; + private static final int TAG_LENGTH = 16; + + private final AeadCrypter aeadCrypter; + + private final byte[] outCounter = new byte[COUNTER_LENGTH]; + private final byte[] inCounter = new byte[COUNTER_LENGTH]; + private final byte[] oldCounter = new byte[COUNTER_LENGTH]; + + AltsChannelCrypter(byte[] key, boolean isClient) { + checkArgument(key.length == KEY_LENGTH); + byte[] counter = isClient ? inCounter : outCounter; + counter[counter.length - 1] = (byte) 0x80; + this.aeadCrypter = new AesGcmHkdfAeadCrypter(key); + } + + static int getKeyLength() { + return KEY_LENGTH; + } + + static int getCounterLength() { + return COUNTER_LENGTH; + } + + @Override + public void encrypt(ByteBuf outBuf, List plainBufs) throws GeneralSecurityException { + checkArgument(outBuf.nioBufferCount() == 1); + // Copy plaintext buffers into outBuf for in-place encryption on single direct buffer. + ByteBuf plainBuf = outBuf.slice(outBuf.writerIndex(), outBuf.writableBytes()); + plainBuf.writerIndex(0); + for (ByteBuf inBuf : plainBufs) { + plainBuf.writeBytes(inBuf); + } + + verify(outBuf.writableBytes() == plainBuf.readableBytes() + TAG_LENGTH); + ByteBuffer out = outBuf.internalNioBuffer(outBuf.writerIndex(), outBuf.writableBytes()); + ByteBuffer plain = out.duplicate(); + plain.limit(out.limit() - TAG_LENGTH); + + byte[] counter = incrementOutCounter(); + int outPosition = out.position(); + aeadCrypter.encrypt(out, plain, counter); + int bytesWritten = out.position() - outPosition; + outBuf.writerIndex(outBuf.writerIndex() + bytesWritten); + verify(!outBuf.isWritable()); + } + + @Override + public void decrypt(ByteBuf out, ByteBuf tag, List ciphertextBufs) + throws GeneralSecurityException { + + ByteBuf cipherTextAndTag = out.slice(out.writerIndex(), out.writableBytes()); + cipherTextAndTag.writerIndex(0); + + for (ByteBuf inBuf : ciphertextBufs) { + cipherTextAndTag.writeBytes(inBuf); + } + cipherTextAndTag.writeBytes(tag); + + decrypt(out, cipherTextAndTag); + } + + @Override + public void decrypt(ByteBuf out, ByteBuf ciphertextAndTag) throws GeneralSecurityException { + int bytesRead = ciphertextAndTag.readableBytes(); + checkArgument(bytesRead == out.writableBytes()); + + checkArgument(out.nioBufferCount() == 1); + ByteBuffer outBuffer = out.internalNioBuffer(out.writerIndex(), out.writableBytes()); + + checkArgument(ciphertextAndTag.nioBufferCount() == 1); + ByteBuffer ciphertextAndTagBuffer = + ciphertextAndTag.nioBuffer(ciphertextAndTag.readerIndex(), bytesRead); + + byte[] counter = incrementInCounter(); + int outPosition = outBuffer.position(); + aeadCrypter.decrypt(outBuffer, ciphertextAndTagBuffer, counter); + int bytesWritten = outBuffer.position() - outPosition; + out.writerIndex(out.writerIndex() + bytesWritten); + ciphertextAndTag.readerIndex(out.readerIndex() + bytesRead); + verify(out.writableBytes() == TAG_LENGTH); + } + + @Override + public int getSuffixLength() { + return TAG_LENGTH; + } + + @Override + public void destroy() { + // no destroy required + } + + /** Increments {@code counter}, store the unincremented value in {@code oldCounter}. */ + static void incrementCounter(byte[] counter, byte[] oldCounter) throws GeneralSecurityException { + System.arraycopy(counter, 0, oldCounter, 0, counter.length); + int i = 0; + for (; i < COUNTER_OVERFLOW_LENGTH; i++) { + counter[i]++; + if (counter[i] != (byte) 0x00) { + break; + } + } + + if (i == COUNTER_OVERFLOW_LENGTH) { + // Restore old counter value to ensure that encrypt and decrypt keep failing. + System.arraycopy(oldCounter, 0, counter, 0, counter.length); + throw new GeneralSecurityException("Counter has overflowed."); + } + } + + /** Increments the input counter, returning the previous (unincremented) value. */ + private byte[] incrementInCounter() throws GeneralSecurityException { + incrementCounter(inCounter, oldCounter); + return oldCounter; + } + + /** Increments the output counter, returning the previous (unincremented) value. */ + private byte[] incrementOutCounter() throws GeneralSecurityException { + incrementCounter(outCounter, oldCounter); + return oldCounter; + } + + @VisibleForTesting + void incrementInCounterForTesting(int n) throws GeneralSecurityException { + for (int i = 0; i < n; i++) { + incrementInCounter(); + } + } + + @VisibleForTesting + void incrementOutCounterForTesting(int n) throws GeneralSecurityException { + for (int i = 0; i < n; i++) { + incrementOutCounter(); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsClientOptions.java b/alts/src/main/java/io/grpc/alts/internal/AltsClientOptions.java new file mode 100644 index 00000000000..9ad614c9623 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsClientOptions.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import com.google.common.collect.ImmutableList; +import javax.annotation.Nullable; + +/** Handshaker options for creating ALTS client channel. */ +public final class AltsClientOptions extends AltsHandshakerOptions { + + // targetName is the server service account name for secure name checking. + @Nullable private final String targetName; + // targetServiceAccounts contains a list of expected target service accounts. One of these service + // accounts should match peer service account in the handshaker result. Otherwise, the handshake + // fails. + private final ImmutableList targetServiceAccounts; + + private AltsClientOptions(Builder builder) { + super(builder.rpcProtocolVersions); + targetName = builder.targetName; + targetServiceAccounts = builder.targetServiceAccounts; + } + + public String getTargetName() { + return targetName; + } + + public ImmutableList getTargetServiceAccounts() { + return targetServiceAccounts; + } + + /** Builder for AltsClientOptions. */ + public static final class Builder { + + @Nullable private String targetName; + @Nullable private RpcProtocolVersions rpcProtocolVersions; + private ImmutableList targetServiceAccounts = ImmutableList.of(); + + public Builder setTargetName(String targetName) { + this.targetName = targetName; + return this; + } + + public Builder setRpcProtocolVersions(RpcProtocolVersions rpcProtocolVersions) { + this.rpcProtocolVersions = rpcProtocolVersions; + return this; + } + + public Builder setTargetServiceAccounts(ImmutableList targetServiceAccounts) { + this.targetServiceAccounts = targetServiceAccounts; + return this; + } + + public AltsClientOptions build() { + return new AltsClientOptions(this); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsFraming.java b/alts/src/main/java/io/grpc/alts/internal/AltsFraming.java new file mode 100644 index 00000000000..2571f937225 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsFraming.java @@ -0,0 +1,366 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import com.google.common.base.Preconditions; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.GeneralSecurityException; + +/** Framing and deframing methods and classes used by handshaker. */ +public final class AltsFraming { + // The size of the frame field. Must correspond to the size of int, 4 bytes. + // Left package-private for testing. + private static final int FRAME_LENGTH_HEADER_SIZE = 4; + private static final int FRAME_MESSAGE_TYPE_HEADER_SIZE = 4; + private static final int MAX_DATA_LENGTH = 1024 * 1024; + private static final int INITIAL_BUFFER_CAPACITY = 1024 * 64; + + // TODO: Make this the responsibility of the caller. + private static final int MESSAGE_TYPE = 6; + + private AltsFraming() {} + + static int getFrameLengthHeaderSize() { + return FRAME_LENGTH_HEADER_SIZE; + } + + static int getFrameMessageTypeHeaderSize() { + return FRAME_MESSAGE_TYPE_HEADER_SIZE; + } + + static int getMaxDataLength() { + return MAX_DATA_LENGTH; + } + + static int getFramingOverhead() { + return FRAME_LENGTH_HEADER_SIZE + FRAME_MESSAGE_TYPE_HEADER_SIZE; + } + + /** + * Creates a frame of length dataSize + FRAME_HEADER_SIZE using the input bytes, if dataSize <= + * input.remaining(). Otherwise, a frame of length input.remaining() + FRAME_HEADER_SIZE is + * created. + */ + static ByteBuffer toFrame(ByteBuffer input, int dataSize) throws GeneralSecurityException { + Preconditions.checkNotNull(input); + if (dataSize > input.remaining()) { + dataSize = input.remaining(); + } + Producer producer = new Producer(); + ByteBuffer inputAlias = input.duplicate(); + ((Buffer) inputAlias).limit(input.position() + dataSize); + producer.readBytes(inputAlias); + producer.flush(); + ((Buffer) input).position(inputAlias.position()); + ByteBuffer output = producer.getRawFrame(); + return output; + } + + /** + * A helper class to write a frame. + * + *

This class guarantees that one of the following is true: + * + *

    + *
  • readBytes will read from the input + *
  • writeBytes will write to the output + *
+ * + *

Sample usage: + * + *

{@code
+   * Producer producer = new Producer();
+   * ByteBuffer inputBuffer = readBytesFromMyStream();
+   * ByteBuffer outputBuffer = writeBytesToMyStream();
+   * while (inputBuffer.hasRemaining() || outputBuffer.hasRemaining()) {
+   *   producer.readBytes(inputBuffer);
+   *   producer.writeBytes(outputBuffer);
+   * }
+   * }
+ * + *

Alternatively, this class guarantees that one of the following is true: + * + *

    + *
  • readBytes will read from the input + *
  • {@code isComplete()} returns true and {@code getByteBuffer()} returns the contents of a + * processed frame. + *
+ * + *

Sample usage: + * + *

{@code
+   * Producer producer = new Producer();
+   * while (!producer.isComplete()) {
+   *   ByteBuffer inputBuffer = readBytesFromMyStream();
+   *   producer.readBytes(inputBuffer);
+   * }
+   * producer.flush();
+   * ByteBuffer outputBuffer = producer.getRawFrame();
+   * }
+ */ + static final class Producer { + private ByteBuffer buffer; + private boolean isComplete; + + Producer(int maxFrameSize) { + buffer = ByteBuffer.allocate(maxFrameSize); + reset(); + Preconditions.checkArgument(maxFrameSize > getFramePrefixLength() + getFrameSuffixLength()); + } + + Producer() { + this(INITIAL_BUFFER_CAPACITY); + } + + /** The length of the frame prefix data, including the message length/type fields. */ + int getFramePrefixLength() { + int result = FRAME_LENGTH_HEADER_SIZE + FRAME_MESSAGE_TYPE_HEADER_SIZE; + return result; + } + + int getFrameSuffixLength() { + return 0; + } + + /** + * Reads bytes from input, parsing them into a frame. Returns false if and only if more data is + * needed. To obtain a full frame this method must be called repeatedly until it returns true. + */ + boolean readBytes(ByteBuffer input) throws GeneralSecurityException { + Preconditions.checkNotNull(input); + if (isComplete) { + return true; + } + copy(buffer, input); + if (!buffer.hasRemaining()) { + flush(); + } + return isComplete; + } + + /** + * Completes the current frame, signaling that no further data is available to be passed to + * readBytes and that the client requires writeBytes to start returning data. isComplete() is + * guaranteed to return true after this call. + */ + void flush() throws GeneralSecurityException { + if (isComplete) { + return; + } + // Get the length of the complete frame. + int frameLength = buffer.position() + getFrameSuffixLength(); + + // Set the limit and move to the start. + ((Buffer) buffer).flip(); + + // Advance the limit to allow a crypto suffix. + ((Buffer) buffer).limit(buffer.limit() + getFrameSuffixLength()); + + // Write the data length and the message type. + int dataLength = frameLength - FRAME_LENGTH_HEADER_SIZE; + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(dataLength); + buffer.putInt(MESSAGE_TYPE); + + // Move the position back to 0, the frame is ready. + ((Buffer) buffer).position(0); + isComplete = true; + } + + /** Resets the state, preparing to construct a new frame. Must be called between frames. */ + private void reset() { + ((Buffer) buffer).clear(); + + // Save some space for framing, we'll fill that in later. + ((Buffer) buffer).position(getFramePrefixLength()); + ((Buffer) buffer).limit(buffer.limit() - getFrameSuffixLength()); + + isComplete = false; + } + + /** + * Returns a ByteBuffer containing a complete raw frame, if it's available. Should only be + * called when isComplete() returns true, otherwise null is returned. The returned object + * aliases the internal buffer, that is, it shares memory with the internal buffer. No further + * operations are permitted on this object until the caller has processed the data it needs from + * the returned byte buffer. + */ + ByteBuffer getRawFrame() { + if (!isComplete) { + return null; + } + ByteBuffer result = buffer.duplicate(); + reset(); + return result; + } + } + + /** + * A helper class to read a frame. + * + *

This class guarantees that one of the following is true: + * + *

    + *
  • readBytes will read from the input + *
  • writeBytes will write to the output + *
+ * + *

Sample usage: + * + *

{@code
+   * Parser parser = new Parser();
+   * ByteBuffer inputBuffer = readBytesFromMyStream();
+   * ByteBuffer outputBuffer = writeBytesToMyStream();
+   * while (inputBuffer.hasRemaining() || outputBuffer.hasRemaining()) {
+   *   parser.readBytes(inputBuffer);
+   *   parser.writeBytes(outputBuffer); }
+   * }
+ * + *

Alternatively, this class guarantees that one of the following is true: + * + *

    + *
  • readBytes will read from the input + *
  • {@code isComplete()} returns true and {@code getByteBuffer()} returns the contents of a + * processed frame. + *
+ * + *

Sample usage: + * + *

{@code
+   * Parser parser = new Parser();
+   * while (!parser.isComplete()) {
+   *   ByteBuffer inputBuffer = readBytesFromMyStream();
+   *   parser.readBytes(inputBuffer);
+   * }
+   * ByteBuffer outputBuffer = parser.getRawFrame();
+   * }
+ */ + public static final class Parser { + private ByteBuffer buffer = ByteBuffer.allocate(INITIAL_BUFFER_CAPACITY); + private boolean isComplete = false; + + public Parser() { + Preconditions.checkArgument( + INITIAL_BUFFER_CAPACITY > getFramePrefixLength() + getFrameSuffixLength()); + } + + /** + * Reads bytes from input, parsing them into a frame. Returns false if and only if more data is + * needed. To obtain a full frame this method must be called repeatedly until it returns true. + */ + public boolean readBytes(ByteBuffer input) throws GeneralSecurityException { + Preconditions.checkNotNull(input); + + if (isComplete) { + return true; + } + + // Read enough bytes to determine the length + while (buffer.position() < FRAME_LENGTH_HEADER_SIZE && input.hasRemaining()) { + buffer.put(input.get()); + } + + // If we have enough bytes to determine the length, read the length and ensure that our + // internal buffer is large enough. + if (buffer.position() == FRAME_LENGTH_HEADER_SIZE && input.hasRemaining()) { + ByteBuffer bufferAlias = buffer.duplicate(); + ((Buffer) bufferAlias).flip(); + bufferAlias.order(ByteOrder.LITTLE_ENDIAN); + int dataLength = bufferAlias.getInt(); + if (dataLength < FRAME_MESSAGE_TYPE_HEADER_SIZE || dataLength > MAX_DATA_LENGTH) { + throw new IllegalArgumentException("Invalid frame length " + dataLength); + } + // Maybe resize the buffer + int frameLength = dataLength + FRAME_LENGTH_HEADER_SIZE; + if (buffer.capacity() < frameLength) { + buffer = ByteBuffer.allocate(frameLength); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(dataLength); + } + ((Buffer) buffer).limit(frameLength); + } + + // TODO: Similarly extract and check message type. + + // Read the remaining data into the internal buffer. + copy(buffer, input); + if (!buffer.hasRemaining()) { + ((Buffer) buffer).flip(); + isComplete = true; + } + return isComplete; + } + + /** The length of the frame prefix data, including the message length/type fields. */ + int getFramePrefixLength() { + int result = FRAME_LENGTH_HEADER_SIZE + FRAME_MESSAGE_TYPE_HEADER_SIZE; + return result; + } + + int getFrameSuffixLength() { + return 0; + } + + /** Returns true if we've parsed a complete frame. */ + public boolean isComplete() { + return isComplete; + } + + /** Resets the state, preparing to parse a new frame. Must be called between frames. */ + private void reset() { + ((Buffer) buffer).clear(); + isComplete = false; + } + + /** + * Returns a ByteBuffer containing a complete raw frame, if it's available. Should only be + * called when isComplete() returns true, otherwise null is returned. The returned object + * aliases the internal buffer, that is, it shares memory with the internal buffer. No further + * operations are permitted on this object until the caller has processed the data it needs from + * the returned byte buffer. + */ + public ByteBuffer getRawFrame() { + if (!isComplete) { + return null; + } + ByteBuffer result = buffer.duplicate(); + reset(); + return result; + } + } + + /** + * Copy as much as possible to dst from src. Unlike {@link ByteBuffer#put(ByteBuffer)}, this stops + * early if there is no room left in dst. + */ + private static void copy(ByteBuffer dst, ByteBuffer src) { + if (dst.hasRemaining() && src.hasRemaining()) { + // Avoid an allocation if possible. + if (dst.remaining() >= src.remaining()) { + dst.put(src); + } else { + int count = Math.min(dst.remaining(), src.remaining()); + ByteBuffer slice = src.slice(); + ((Buffer) slice).limit(count); + dst.put(slice); + ((Buffer) src).position(src.position() + count); + } + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerClient.java b/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerClient.java new file mode 100644 index 00000000000..9eb07f3e86d --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerClient.java @@ -0,0 +1,252 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.protobuf.ByteString; +import io.grpc.ChannelLogger; +import io.grpc.ChannelLogger.ChannelLogLevel; +import io.grpc.Status; +import io.grpc.alts.internal.HandshakerServiceGrpc.HandshakerServiceStub; +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** An API for conducting handshakes via ALTS handshaker service. */ +class AltsHandshakerClient { + private static final String APPLICATION_PROTOCOL = "grpc"; + private static final String RECORD_PROTOCOL = "ALTSRP_GCM_AES128_REKEY"; + private static final int KEY_LENGTH = AltsChannelCrypter.getKeyLength(); + + private final AltsHandshakerStub handshakerStub; + private final AltsHandshakerOptions handshakerOptions; + private HandshakerResult result; + private HandshakerStatus status; + private final ChannelLogger logger; + + /** Starts a new handshake interacting with the handshaker service. */ + AltsHandshakerClient( + HandshakerServiceStub stub, AltsHandshakerOptions options, ChannelLogger logger) { + handshakerStub = new AltsHandshakerStub(stub); + handshakerOptions = options; + this.logger = logger; + } + + @VisibleForTesting + AltsHandshakerClient( + AltsHandshakerStub handshakerStub, AltsHandshakerOptions options, ChannelLogger logger) { + this.handshakerStub = handshakerStub; + handshakerOptions = options; + this.logger = logger; + } + + static String getApplicationProtocol() { + return APPLICATION_PROTOCOL; + } + + static String getRecordProtocol() { + return RECORD_PROTOCOL; + } + + /** Sets the start client fields for the passed handshake request. */ + private void setStartClientFields(HandshakerReq.Builder req) { + // Sets the default values. + StartClientHandshakeReq.Builder startClientReq = + StartClientHandshakeReq.newBuilder() + .setHandshakeSecurityProtocol(HandshakeProtocol.ALTS) + .addApplicationProtocols(APPLICATION_PROTOCOL) + .addRecordProtocols(RECORD_PROTOCOL); + // Sets handshaker options. + if (handshakerOptions.getRpcProtocolVersions() != null) { + startClientReq.setRpcVersions(handshakerOptions.getRpcProtocolVersions()); + } + if (handshakerOptions instanceof AltsClientOptions) { + AltsClientOptions clientOptions = (AltsClientOptions) handshakerOptions; + if (!Strings.isNullOrEmpty(clientOptions.getTargetName())) { + startClientReq.setTargetName(clientOptions.getTargetName()); + } + for (String serviceAccount : clientOptions.getTargetServiceAccounts()) { + startClientReq.addTargetIdentitiesBuilder().setServiceAccount(serviceAccount); + } + } + startClientReq.setMaxFrameSize(AltsTsiFrameProtector.getMaxFrameSize()); + req.setClientStart(startClientReq); + } + + /** Sets the start server fields for the passed handshake request. */ + private void setStartServerFields(HandshakerReq.Builder req, ByteBuffer inBytes) { + ServerHandshakeParameters serverParameters = + ServerHandshakeParameters.newBuilder().addRecordProtocols(RECORD_PROTOCOL).build(); + StartServerHandshakeReq.Builder startServerReq = + StartServerHandshakeReq.newBuilder() + .addApplicationProtocols(APPLICATION_PROTOCOL) + .putHandshakeParameters(HandshakeProtocol.ALTS.getNumber(), serverParameters) + .setInBytes(ByteString.copyFrom(inBytes.duplicate())); + if (handshakerOptions.getRpcProtocolVersions() != null) { + startServerReq.setRpcVersions(handshakerOptions.getRpcProtocolVersions()); + } + startServerReq.setMaxFrameSize(AltsTsiFrameProtector.getMaxFrameSize()); + req.setServerStart(startServerReq); + } + + /** Returns true if the handshake is complete. */ + public boolean isFinished() { + // If we have a HandshakeResult, we are done. + if (result != null) { + return true; + } + // If we have an error status, we are done. + if (status != null && status.getCode() != Status.Code.OK.value()) { + return true; + } + return false; + } + + /** Returns the handshake status. */ + public HandshakerStatus getStatus() { + return status; + } + + /** Returns the result data of the handshake, if the handshake is completed. */ + public HandshakerResult getResult() { + return result; + } + + /** + * Returns the resulting key of the handshake, if the handshake is completed. Note that the key + * data returned from the handshake may be more than the key length required for the record + * protocol, thus we need to truncate to the right size. + */ + public byte[] getKey() { + if (result == null) { + return null; + } + if (result.getKeyData().size() < KEY_LENGTH) { + throw new IllegalStateException("Could not get enough key data from the handshake."); + } + byte[] key = new byte[KEY_LENGTH]; + result.getKeyData().substring(0, KEY_LENGTH).copyTo(key, 0); + return key; + } + + /** + * Parses a handshake response, setting the status, result, and closing the handshaker, as needed. + */ + private void handleResponse(HandshakerResp resp) throws GeneralSecurityException { + status = resp.getStatus(); + if (resp.hasResult()) { + result = resp.getResult(); + close(); + } + if (status.getCode() != Status.Code.OK.value()) { + String error = "Handshaker service error: " + status.getDetails(); + logger.log(ChannelLogLevel.DEBUG, error); + close(); + throw new GeneralSecurityException(error); + } + } + + /** + * Starts a client handshake. A GeneralSecurityException is thrown if the handshaker service is + * interrupted or fails. Note that isFinished() must be false before this function is called. + * + * @return the frame to give to the peer. + * @throws GeneralSecurityException or IllegalStateException + */ + public ByteBuffer startClientHandshake() throws GeneralSecurityException { + Preconditions.checkState(!isFinished(), "Handshake has already finished."); + HandshakerReq.Builder req = HandshakerReq.newBuilder(); + setStartClientFields(req); + HandshakerResp resp; + try { + logger.log(ChannelLogLevel.DEBUG, "Send ALTS handshake request to upstream"); + resp = handshakerStub.send(req.build()); + logger.log(ChannelLogLevel.DEBUG, "Receive ALTS handshake response from upstream"); + } catch (IOException | InterruptedException e) { + throw new GeneralSecurityException(e); + } + handleResponse(resp); + return resp.getOutFrames().asReadOnlyByteBuffer(); + } + + /** + * Starts a server handshake. A GeneralSecurityException is thrown if the handshaker service is + * interrupted or fails. Note that isFinished() must be false before this function is called. + * + * @param inBytes the bytes received from the peer. + * @return the frame to give to the peer. + * @throws GeneralSecurityException or IllegalStateException + */ + public ByteBuffer startServerHandshake(ByteBuffer inBytes) throws GeneralSecurityException { + Preconditions.checkState(!isFinished(), "Handshake has already finished."); + HandshakerReq.Builder req = HandshakerReq.newBuilder(); + setStartServerFields(req, inBytes); + HandshakerResp resp; + try { + resp = handshakerStub.send(req.build()); + } catch (IOException | InterruptedException e) { + throw new GeneralSecurityException(e); + } + handleResponse(resp); + ((Buffer) inBytes).position(inBytes.position() + resp.getBytesConsumed()); + return resp.getOutFrames().asReadOnlyByteBuffer(); + } + + /** + * Processes the next bytes in a handshake. A GeneralSecurityException is thrown if the handshaker + * service is interrupted or fails. Note that isFinished() must be false before this function is + * called. + * + * @param inBytes the bytes received from the peer. + * @return the frame to give to the peer. + * @throws GeneralSecurityException or IllegalStateException + */ + public ByteBuffer next(ByteBuffer inBytes) throws GeneralSecurityException { + Preconditions.checkState(!isFinished(), "Handshake has already finished."); + HandshakerReq.Builder req = + HandshakerReq.newBuilder() + .setNext( + NextHandshakeMessageReq.newBuilder() + .setInBytes(ByteString.copyFrom(inBytes.duplicate())) + .build()); + HandshakerResp resp; + try { + logger.log(ChannelLogLevel.DEBUG, "Send ALTS handshake request to upstream"); + resp = handshakerStub.send(req.build()); + logger.log(ChannelLogLevel.DEBUG, "Receive ALTS handshake response from upstream"); + } catch (IOException | InterruptedException e) { + throw new GeneralSecurityException(e); + } + handleResponse(resp); + ((Buffer) inBytes).position(inBytes.position() + resp.getBytesConsumed()); + return resp.getOutFrames().asReadOnlyByteBuffer(); + } + + private boolean closed = false; + + /** Closes the connection. */ + public void close() { + if (closed) { + return; + } + closed = true; + handshakerStub.close(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerOptions.java b/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerOptions.java new file mode 100644 index 00000000000..d50f92f185f --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerOptions.java @@ -0,0 +1,32 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import javax.annotation.Nullable; + +/** Handshaker options for creating ALTS channel. */ +public class AltsHandshakerOptions { + @Nullable private final RpcProtocolVersions rpcProtocolVersions; + + public AltsHandshakerOptions(RpcProtocolVersions rpcProtocolVersions) { + this.rpcProtocolVersions = rpcProtocolVersions; + } + + public RpcProtocolVersions getRpcProtocolVersions() { + return rpcProtocolVersions; + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerStub.java b/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerStub.java new file mode 100644 index 00000000000..6c2748dcc9c --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsHandshakerStub.java @@ -0,0 +1,147 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import io.grpc.alts.internal.HandshakerServiceGrpc.HandshakerServiceStub; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; + +/** An interface to the ALTS handshaker service. */ +class AltsHandshakerStub { + private final StreamObserver reader = new Reader(); + private StreamObserver writer; + private final HandshakerServiceStub serviceStub; + private final ArrayBlockingQueue> responseQueue = + new ArrayBlockingQueue<>(1); + private final AtomicReference exceptionMessage = new AtomicReference<>(); + + private static final long HANDSHAKE_RPC_DEADLINE_SECS = 20; + + AltsHandshakerStub(HandshakerServiceStub serviceStub) { + this.serviceStub = serviceStub; + } + + @VisibleForTesting + AltsHandshakerStub() { + serviceStub = null; + } + + @VisibleForTesting + AltsHandshakerStub(StreamObserver writer) { + this.writer = writer; + serviceStub = null; + } + + @VisibleForTesting + StreamObserver getReaderForTest() { + return reader; + } + + /** Send a handshaker request and return the handshaker response. */ + public HandshakerResp send(HandshakerReq req) throws InterruptedException, IOException { + createWriterIfNull(); + maybeThrowIoException(); + if (!responseQueue.isEmpty()) { + throw new IOException("Received an unexpected response."); + } + + writer.onNext(req); + Optional result = responseQueue.take(); + if (result.isPresent()) { + return result.get(); + } + + if (exceptionMessage.get() != null) { + throw new IOException(exceptionMessage.get().info, exceptionMessage.get().throwable); + } else { + throw new IOException("No handshaker response received"); + } + } + + /** Create a new writer if the writer is null. */ + private void createWriterIfNull() { + if (writer == null) { + writer = + serviceStub.withDeadlineAfter(HANDSHAKE_RPC_DEADLINE_SECS, SECONDS).doHandshake(reader); + } + } + + /** Throw exception if there is an outstanding exception. */ + private void maybeThrowIoException() throws IOException { + if (exceptionMessage.get() != null) { + throw new IOException(exceptionMessage.get().info, exceptionMessage.get().throwable); + } + } + + /** Close the connection. */ + public void close() { + if (writer != null) { + writer.onCompleted(); + } + } + + private class Reader implements StreamObserver { + /** Receive a handshaker response from the server. */ + @Override + public void onNext(HandshakerResp resp) { + try { + AltsHandshakerStub.this.responseQueue.add(Optional.of(resp)); + } catch (IllegalStateException e) { + AltsHandshakerStub.this.exceptionMessage.compareAndSet( + null, new ThrowableInfo(e, "Received an unexpected response.")); + AltsHandshakerStub.this.close(); + } + } + + /** Receive an error from the server. */ + @Override + public void onError(Throwable t) { + AltsHandshakerStub.this.exceptionMessage.compareAndSet( + null, new ThrowableInfo(t, "Received a terminating error.")); + // Trigger the release of any blocked send. + Optional result = Optional.absent(); + AltsHandshakerStub.this.responseQueue.offer(result); + } + + /** Receive the closing message from the server. */ + @Override + public void onCompleted() { + AltsHandshakerStub.this.exceptionMessage.compareAndSet( + null, new ThrowableInfo(null, "Response stream closed.")); + // Trigger the release of any blocked send. + Optional result = Optional.absent(); + AltsHandshakerStub.this.responseQueue.offer(result); + } + } + + private static class ThrowableInfo { + + private final Throwable throwable; + private final String info; + + private ThrowableInfo(Throwable throwable, String info) { + this.throwable = throwable; + this.info = info; + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsInternalContext.java b/alts/src/main/java/io/grpc/alts/internal/AltsInternalContext.java new file mode 100644 index 00000000000..a70c67b2933 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsInternalContext.java @@ -0,0 +1,108 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Map; + +/** AltsInternalContext contains security-related context information about an ALTs connection. */ +public final class AltsInternalContext { + final AltsContext context; + + /** Create a new AltsInternalContext. */ + public AltsInternalContext(HandshakerResult result) { + context = + AltsContext.newBuilder() + .setApplicationProtocol(result.getApplicationProtocol()) + .setRecordProtocol(result.getRecordProtocol()) + // TODO: Set security level based on the handshaker result. + .setSecurityLevel(SecurityLevel.INTEGRITY_AND_PRIVACY) + .setPeerServiceAccount(result.getPeerIdentity().getServiceAccount()) + .setLocalServiceAccount(result.getLocalIdentity().getServiceAccount()) + .setPeerRpcVersions(result.getPeerRpcVersions()) + .putAllPeerAttributes(result.getPeerIdentity().getAttributesMap()) + .build(); + } + + @VisibleForTesting + public static AltsInternalContext getDefaultInstance() { + return new AltsInternalContext(HandshakerResult.newBuilder().build()); + } + + /** + * Get application protocol. + * + * @return the context's application protocol. + */ + public String getApplicationProtocol() { + return context.getApplicationProtocol(); + } + + /** + * Get negotiated record protocol. + * + * @return the context's negotiated record protocol. + */ + public String getRecordProtocol() { + return context.getRecordProtocol(); + } + + /** + * Get security level. + * + * @return the context's security level. + */ + public SecurityLevel getSecurityLevel() { + return context.getSecurityLevel(); + } + + /** + * Get peer service account. + * + * @return the context's peer service account. + */ + public String getPeerServiceAccount() { + return context.getPeerServiceAccount(); + } + + /** + * Get local service account. + * + * @return the context's local service account. + */ + public String getLocalServiceAccount() { + return context.getLocalServiceAccount(); + } + + /** + * Get peer RPC versions. + * + * @return the context's peer RPC versions. + */ + public RpcProtocolVersions getPeerRpcVersions() { + return context.getPeerRpcVersions(); + } + + /** + * Get peer attributes. + * + * @return the context's peer attributes. + */ + public Map getPeerAttributes() { + return context.getPeerAttributesMap(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java b/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java new file mode 100644 index 00000000000..9c51cf6a053 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java @@ -0,0 +1,452 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Any; +import io.grpc.Attributes; +import io.grpc.Channel; +import io.grpc.ChannelLogger; +import io.grpc.Grpc; +import io.grpc.InternalChannelz.OtherSecurity; +import io.grpc.InternalChannelz.Security; +import io.grpc.SecurityLevel; +import io.grpc.Status; +import io.grpc.alts.internal.RpcProtocolVersionsUtil.RpcVersionsCheckResult; +import io.grpc.internal.ObjectPool; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.InternalProtocolNegotiator; +import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; +import io.grpc.netty.InternalProtocolNegotiators; +import io.netty.channel.ChannelHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.util.AsciiString; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * A gRPC {@link ProtocolNegotiator} for ALTS. This class creates a Netty handler that provides ALTS + * security on the wire, similar to Netty's {@code SslHandler}. + */ +// TODO(carl-mastrangelo): rename this AltsProtocolNegotiators. +public final class AltsProtocolNegotiator { + private static final Logger logger = Logger.getLogger(AltsProtocolNegotiator.class.getName()); + + static final String ALTS_MAX_CONCURRENT_HANDSHAKES_ENV_VARIABLE = + "GRPC_ALTS_MAX_CONCURRENT_HANDSHAKES"; + @VisibleForTesting static final int DEFAULT_ALTS_MAX_CONCURRENT_HANDSHAKES = 32; + private static final AsyncSemaphore handshakeSemaphore = + new AsyncSemaphore(getAltsMaxConcurrentHandshakes()); + + @Grpc.TransportAttr + public static final Attributes.Key TSI_PEER_KEY = + Attributes.Key.create("internal:TSI_PEER"); + @Grpc.TransportAttr + public static final Attributes.Key AUTH_CONTEXT_KEY = + Attributes.Key.create("internal:AUTH_CONTEXT_KEY"); + + private static final AsciiString SCHEME = AsciiString.of("https"); + + private static final String DIRECT_PATH_SERVICE_CFE_CLUSTER_PREFIX = "google_cfe_"; + private static final String CFE_CLUSTER_RESOURCE_NAME_PREFIX = + "/envoy.config.cluster.v3.Cluster/google_cfe_"; + private static final String CFE_CLUSTER_AUTHORITY_NAME = + "traffic-director-c2p.xds.googleapis.com"; + + /** + * ClientAltsProtocolNegotiatorFactory is a factory for doing client side negotiation of an ALTS + * channel. + */ + public static final class ClientAltsProtocolNegotiatorFactory + implements InternalProtocolNegotiator.ClientFactory { + + private final ImmutableList targetServiceAccounts; + private final ObjectPool handshakerChannelPool; + + public ClientAltsProtocolNegotiatorFactory( + List targetServiceAccounts, + ObjectPool handshakerChannelPool) { + this.targetServiceAccounts = ImmutableList.copyOf(targetServiceAccounts); + this.handshakerChannelPool = checkNotNull(handshakerChannelPool, "handshakerChannelPool"); + } + + @Override + public ProtocolNegotiator newNegotiator() { + return new ClientAltsProtocolNegotiator( + targetServiceAccounts, + handshakerChannelPool); + } + + @Override + public int getDefaultPort() { + return 443; + } + } + + private static final class ClientAltsProtocolNegotiator implements ProtocolNegotiator { + private final TsiHandshakerFactory handshakerFactory; + private final LazyChannel lazyHandshakerChannel; + + ClientAltsProtocolNegotiator( + ImmutableList targetServiceAccounts, ObjectPool handshakerChannelPool) { + this.lazyHandshakerChannel = new LazyChannel(handshakerChannelPool); + this.handshakerFactory = + new ClientTsiHandshakerFactory(targetServiceAccounts, lazyHandshakerChannel); + } + + @Override + public AsciiString scheme() { + return SCHEME; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + ChannelLogger negotiationLogger = grpcHandler.getNegotiationLogger(); + TsiHandshaker handshaker = + handshakerFactory.newHandshaker(grpcHandler.getAuthority(), negotiationLogger); + NettyTsiHandshaker nettyHandshaker = new NettyTsiHandshaker(handshaker); + ChannelHandler gnh = InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler); + ChannelHandler thh = new TsiHandshakeHandler( + gnh, nettyHandshaker, new AltsHandshakeValidator(), handshakeSemaphore, + negotiationLogger); + ChannelHandler wuah = InternalProtocolNegotiators.waitUntilActiveHandler(thh, + negotiationLogger); + return wuah; + } + + @Override + public void close() { + lazyHandshakerChannel.close(); + } + } + + /** + * Creates a protocol negotiator for ALTS on the server side. + */ + public static ProtocolNegotiator serverAltsProtocolNegotiator( + ObjectPool handshakerChannelPool) { + final LazyChannel lazyHandshakerChannel = new LazyChannel(handshakerChannelPool); + final class ServerTsiHandshakerFactory implements TsiHandshakerFactory { + + @Override + public TsiHandshaker newHandshaker( + @Nullable String authority, ChannelLogger negotiationLogger) { + assert authority == null; + return AltsTsiHandshaker.newServer( + HandshakerServiceGrpc.newStub(lazyHandshakerChannel.get()), + new AltsHandshakerOptions(RpcProtocolVersionsUtil.getRpcProtocolVersions()), + negotiationLogger); + } + } + + return new ServerAltsProtocolNegotiator( + new ServerTsiHandshakerFactory(), lazyHandshakerChannel); + } + + @VisibleForTesting + static final class ServerAltsProtocolNegotiator implements ProtocolNegotiator { + private final TsiHandshakerFactory handshakerFactory; + private final LazyChannel lazyHandshakerChannel; + + @VisibleForTesting + ServerAltsProtocolNegotiator( + TsiHandshakerFactory handshakerFactory, LazyChannel lazyHandshakerChannel) { + this.handshakerFactory = checkNotNull(handshakerFactory, "handshakerFactory"); + this.lazyHandshakerChannel = checkNotNull(lazyHandshakerChannel, "lazyHandshakerChannel"); + } + + @Override + public AsciiString scheme() { + return SCHEME; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + ChannelLogger negotiationLogger = grpcHandler.getNegotiationLogger(); + TsiHandshaker handshaker = + handshakerFactory.newHandshaker(/* authority= */ null, negotiationLogger); + NettyTsiHandshaker nettyHandshaker = new NettyTsiHandshaker(handshaker); + ChannelHandler gnh = InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler); + ChannelHandler thh = new TsiHandshakeHandler( + gnh, nettyHandshaker, new AltsHandshakeValidator(), handshakeSemaphore, + negotiationLogger); + ChannelHandler wuah = InternalProtocolNegotiators.waitUntilActiveHandler(thh, + negotiationLogger); + return wuah; + } + + @Override + public void close() { + logger.finest("ALTS Server ProtocolNegotiator Closed"); + lazyHandshakerChannel.close(); + } + } + + /** + * A Protocol Negotiator factory which can switch between ALTS and TLS based on EAG Attrs. + */ + public static final class GoogleDefaultProtocolNegotiatorFactory + implements InternalProtocolNegotiator.ClientFactory { + @VisibleForTesting + @Nullable + static Attributes.Key clusterNameAttrKey = loadClusterNameAttrKey(); + private final ImmutableList targetServiceAccounts; + private final ObjectPool handshakerChannelPool; + private final SslContext sslContext; + + /** + * Creates Negotiator Factory, which will either use the targetServiceAccounts and + * handshakerChannelPool, or the sslContext. + */ + public GoogleDefaultProtocolNegotiatorFactory( + List targetServiceAccounts, + ObjectPool handshakerChannelPool, + SslContext sslContext) { + this.targetServiceAccounts = ImmutableList.copyOf(targetServiceAccounts); + this.handshakerChannelPool = checkNotNull(handshakerChannelPool, "handshakerChannelPool"); + this.sslContext = checkNotNull(sslContext, "sslContext"); + } + + @Override + public ProtocolNegotiator newNegotiator() { + return new GoogleDefaultProtocolNegotiator( + targetServiceAccounts, + handshakerChannelPool, + sslContext, + clusterNameAttrKey); + } + + @Override + public int getDefaultPort() { + return 443; + } + + @SuppressWarnings("unchecked") + @Nullable + private static Attributes.Key loadClusterNameAttrKey() { + Attributes.Key key = null; + try { + Class klass = Class.forName("io.grpc.xds.InternalXdsAttributes"); + key = (Attributes.Key) klass.getField("ATTR_CLUSTER_NAME").get(null); + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, + "Unable to load xDS endpoint cluster name key, this may be expected", e); + } catch (NoSuchFieldException e) { + logger.log(Level.FINE, + "Unable to load xDS endpoint cluster name key, this may be expected", e); + } catch (IllegalAccessException e) { + logger.log(Level.FINE, + "Unable to load xDS endpoint cluster name key, this may be expected", e); + } + return key; + } + } + + private static final class GoogleDefaultProtocolNegotiator implements ProtocolNegotiator { + private final TsiHandshakerFactory handshakerFactory; + private final LazyChannel lazyHandshakerChannel; + private final SslContext sslContext; + @Nullable + private final Attributes.Key clusterNameAttrKey; + + GoogleDefaultProtocolNegotiator( + ImmutableList targetServiceAccounts, + ObjectPool handshakerChannelPool, + SslContext sslContext, + @Nullable Attributes.Key clusterNameAttrKey) { + this.lazyHandshakerChannel = new LazyChannel(handshakerChannelPool); + this.handshakerFactory = + new ClientTsiHandshakerFactory(targetServiceAccounts, lazyHandshakerChannel); + this.sslContext = checkNotNull(sslContext, "checkNotNull"); + this.clusterNameAttrKey = clusterNameAttrKey; + } + + @Override + public AsciiString scheme() { + return SCHEME; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + ChannelHandler gnh = InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler); + ChannelLogger negotiationLogger = grpcHandler.getNegotiationLogger(); + ChannelHandler securityHandler; + boolean isXdsDirectPath = false; + if (clusterNameAttrKey != null) { + isXdsDirectPath = isDirectPathCluster( + grpcHandler.getEagAttributes().get(clusterNameAttrKey)); + } + if (isXdsDirectPath) { + TsiHandshaker handshaker = + handshakerFactory.newHandshaker(grpcHandler.getAuthority(), negotiationLogger); + NettyTsiHandshaker nettyHandshaker = new NettyTsiHandshaker(handshaker); + securityHandler = new TsiHandshakeHandler( + gnh, nettyHandshaker, new AltsHandshakeValidator(), handshakeSemaphore, + negotiationLogger); + } else { + securityHandler = InternalProtocolNegotiators.clientTlsHandler( + gnh, sslContext, grpcHandler.getAuthority(), negotiationLogger); + } + ChannelHandler wuah = InternalProtocolNegotiators.waitUntilActiveHandler(securityHandler, + negotiationLogger); + return wuah; + } + + private boolean isDirectPathCluster(String clusterName) { + if (clusterName == null) { + return false; + } + if (clusterName.startsWith(DIRECT_PATH_SERVICE_CFE_CLUSTER_PREFIX)) { + return false; + } + if (!clusterName.startsWith("xdstp:")) { + return true; + } + try { + URI uri = new URI(clusterName); + // If authority AND path match our CFE checks, use TLS; otherwise use ALTS. + return !CFE_CLUSTER_AUTHORITY_NAME.equals(uri.getHost()) + || !uri.getPath().startsWith(CFE_CLUSTER_RESOURCE_NAME_PREFIX); + } catch (URISyntaxException e) { + return true; // Shouldn't happen, but assume ALTS. + } + } + + @Override + public void close() { + logger.finest("ALTS Server ProtocolNegotiator Closed"); + lazyHandshakerChannel.close(); + } + } + + private static final class ClientTsiHandshakerFactory implements TsiHandshakerFactory { + + private final ImmutableList targetServiceAccounts; + private final LazyChannel lazyHandshakerChannel; + + ClientTsiHandshakerFactory( + ImmutableList targetServiceAccounts, LazyChannel lazyHandshakerChannel) { + this.targetServiceAccounts = checkNotNull(targetServiceAccounts, "targetServiceAccounts"); + this.lazyHandshakerChannel = checkNotNull(lazyHandshakerChannel, "lazyHandshakerChannel"); + } + + @Override + public TsiHandshaker newHandshaker( + @Nullable String authority, ChannelLogger negotiationLogger) { + AltsClientOptions handshakerOptions = + new AltsClientOptions.Builder() + .setRpcProtocolVersions(RpcProtocolVersionsUtil.getRpcProtocolVersions()) + .setTargetServiceAccounts(targetServiceAccounts) + .setTargetName(authority) + .build(); + return AltsTsiHandshaker.newClient( + HandshakerServiceGrpc.newStub(lazyHandshakerChannel.get()), + handshakerOptions, + negotiationLogger); + } + } + + /** Channel created from a channel pool lazily. */ + @VisibleForTesting + static final class LazyChannel { + private final ObjectPool channelPool; + private Channel channel; + + @VisibleForTesting + LazyChannel(ObjectPool channelPool) { + this.channelPool = checkNotNull(channelPool, "channelPool"); + } + + /** + * If channel is null, gets a channel from the channel pool, otherwise, returns the cached + * channel. + */ + synchronized Channel get() { + if (channel == null) { + channel = channelPool.getObject(); + } + return channel; + } + + /** Returns the cached channel to the channel pool. */ + synchronized void close() { + if (channel != null) { + channel = channelPool.returnObject(channel); + } + } + } + + private static final class AltsHandshakeValidator extends TsiHandshakeHandler.HandshakeValidator { + + @Override + public SecurityDetails validatePeerObject(Object peerObject) throws GeneralSecurityException { + AltsInternalContext altsContext = (AltsInternalContext) peerObject; + // Checks peer Rpc Protocol Versions in the ALTS auth context. Fails the connection if + // Rpc Protocol Versions mismatch. + RpcVersionsCheckResult checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersionsUtil.getRpcProtocolVersions(), + altsContext.getPeerRpcVersions()); + if (!checkResult.getResult()) { + String errorMessage = + "Local Rpc Protocol Versions " + + RpcProtocolVersionsUtil.getRpcProtocolVersions() + + " are not compatible with peer Rpc Protocol Versions " + + altsContext.getPeerRpcVersions(); + throw Status.UNAVAILABLE.withDescription(errorMessage).asRuntimeException(); + } + return new SecurityDetails( + SecurityLevel.PRIVACY_AND_INTEGRITY, + new Security(new OtherSecurity("alts", Any.pack(altsContext.context)))); + } + } + + @VisibleForTesting + static int getAltsMaxConcurrentHandshakes(String altsMaxConcurrentHandshakes) { + if (altsMaxConcurrentHandshakes == null) { + return DEFAULT_ALTS_MAX_CONCURRENT_HANDSHAKES; + } + try { + int effectiveMaxConcurrentHandshakes = Integer.parseInt(altsMaxConcurrentHandshakes); + if (effectiveMaxConcurrentHandshakes < 0) { + logger.warning( + "GRPC_ALTS_MAX_CONCURRENT_HANDSHAKES environment variable set to invalid value."); + return DEFAULT_ALTS_MAX_CONCURRENT_HANDSHAKES; + } + return effectiveMaxConcurrentHandshakes; + } catch (NumberFormatException e) { + logger.warning( + "GRPC_ALTS_MAX_CONCURRENT_HANDSHAKES environment variable set to invalid value."); + return DEFAULT_ALTS_MAX_CONCURRENT_HANDSHAKES; + } + } + + private static int getAltsMaxConcurrentHandshakes() { + return getAltsMaxConcurrentHandshakes( + System.getenv(ALTS_MAX_CONCURRENT_HANDSHAKES_ENV_VARIABLE)); + } + + private AltsProtocolNegotiator() {} +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsTsiFrameProtector.java b/alts/src/main/java/io/grpc/alts/internal/AltsTsiFrameProtector.java new file mode 100644 index 00000000000..67d6637a130 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsTsiFrameProtector.java @@ -0,0 +1,408 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; + +import com.google.common.primitives.Ints; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; + +/** Frame protector that uses the ALTS framing. */ +public final class AltsTsiFrameProtector implements TsiFrameProtector { + private static final int HEADER_LEN_FIELD_BYTES = 4; + private static final int HEADER_TYPE_FIELD_BYTES = 4; + private static final int HEADER_BYTES = HEADER_LEN_FIELD_BYTES + HEADER_TYPE_FIELD_BYTES; + private static final int HEADER_TYPE_DEFAULT = 6; + private static final int LIMIT_MAX_ALLOWED_FRAME_SIZE = 1024 * 1024; + // Frame size negotiation extends frame size range to [MIN_FRAME_SIZE, MAX_FRAME_SIZE]. + private static final int MIN_FRAME_SIZE = 16 * 1024; + private static final int MAX_FRAME_SIZE = 128 * 1024; + + private final Protector protector; + private final Unprotector unprotector; + + /** Create a new AltsTsiFrameProtector. */ + public AltsTsiFrameProtector( + int maxProtectedFrameBytes, ChannelCrypterNetty crypter, ByteBufAllocator alloc) { + checkArgument(maxProtectedFrameBytes > HEADER_BYTES + crypter.getSuffixLength()); + maxProtectedFrameBytes = Math.min(LIMIT_MAX_ALLOWED_FRAME_SIZE, maxProtectedFrameBytes); + protector = new Protector(maxProtectedFrameBytes, crypter); + unprotector = new Unprotector(crypter, alloc); + } + + static int getHeaderLenFieldBytes() { + return HEADER_LEN_FIELD_BYTES; + } + + static int getHeaderTypeFieldBytes() { + return HEADER_TYPE_FIELD_BYTES; + } + + public static int getHeaderBytes() { + return HEADER_BYTES; + } + + static int getHeaderTypeDefault() { + return HEADER_TYPE_DEFAULT; + } + + static int getLimitMaxAllowedFrameSize() { + return LIMIT_MAX_ALLOWED_FRAME_SIZE; + } + + public static int getMinFrameSize() { + return MIN_FRAME_SIZE; + } + + public static int getMaxFrameSize() { + return MAX_FRAME_SIZE; + } + + @Override + public void protectFlush( + List unprotectedBufs, Consumer ctxWrite, ByteBufAllocator alloc) + throws GeneralSecurityException { + protector.protectFlush(unprotectedBufs, ctxWrite, alloc); + } + + @Override + public void unprotect(ByteBuf in, List out, ByteBufAllocator alloc) + throws GeneralSecurityException { + unprotector.unprotect(in, out, alloc); + } + + @Override + public void destroy() { + try { + unprotector.destroy(); + } finally { + protector.destroy(); + } + } + + static final class Protector { + private final int maxUnprotectedBytesPerFrame; + private final int suffixBytes; + private ChannelCrypterNetty crypter; + + Protector(int maxProtectedFrameBytes, ChannelCrypterNetty crypter) { + this.suffixBytes = crypter.getSuffixLength(); + this.maxUnprotectedBytesPerFrame = maxProtectedFrameBytes - HEADER_BYTES - suffixBytes; + this.crypter = crypter; + } + + void destroy() { + // Shared with Unprotector and destroyed there. + crypter = null; + } + + void protectFlush( + List unprotectedBufs, Consumer ctxWrite, ByteBufAllocator alloc) + throws GeneralSecurityException { + checkState(crypter != null, "Cannot protectFlush after destroy."); + ByteBuf protectedBuf; + try { + protectedBuf = handleUnprotected(unprotectedBufs, alloc); + } finally { + for (ByteBuf buf : unprotectedBufs) { + buf.release(); + } + } + if (protectedBuf != null) { + ctxWrite.accept(protectedBuf); + } + } + + private ByteBuf handleUnprotected(List unprotectedBufs, ByteBufAllocator alloc) + throws GeneralSecurityException { + long unprotectedBytes = 0; + for (ByteBuf buf : unprotectedBufs) { + unprotectedBytes += buf.readableBytes(); + } + // Empty plaintext not allowed since this should be handled as no-op in layer above. + checkArgument(unprotectedBytes > 0); + + // Compute number of frames and allocate a single buffer for all frames. + long frameNum = unprotectedBytes / maxUnprotectedBytesPerFrame + 1; + int lastFrameUnprotectedBytes = (int) (unprotectedBytes % maxUnprotectedBytesPerFrame); + if (lastFrameUnprotectedBytes == 0) { + frameNum--; + lastFrameUnprotectedBytes = maxUnprotectedBytesPerFrame; + } + long protectedBytes = frameNum * (HEADER_BYTES + suffixBytes) + unprotectedBytes; + + ByteBuf protectedBuf = alloc.directBuffer(Ints.checkedCast(protectedBytes)); + try { + int bufferIdx = 0; + for (int frameIdx = 0; frameIdx < frameNum; ++frameIdx) { + int unprotectedBytesLeft = + (frameIdx == frameNum - 1) ? lastFrameUnprotectedBytes : maxUnprotectedBytesPerFrame; + // Write header (at most LIMIT_MAX_ALLOWED_FRAME_BYTES). + protectedBuf.writeIntLE(unprotectedBytesLeft + HEADER_TYPE_FIELD_BYTES + suffixBytes); + protectedBuf.writeIntLE(HEADER_TYPE_DEFAULT); + + // Ownership of the backing buffer remains with protectedBuf. + ByteBuf frameOut = writeSlice(protectedBuf, unprotectedBytesLeft + suffixBytes); + List framePlain = new ArrayList<>(); + while (unprotectedBytesLeft > 0) { + // Ownership of the buffer backing in remains with unprotectedBufs. + ByteBuf in = unprotectedBufs.get(bufferIdx); + if (in.readableBytes() <= unprotectedBytesLeft) { + // The complete buffer belongs to this frame. + framePlain.add(in); + unprotectedBytesLeft -= in.readableBytes(); + bufferIdx++; + } else { + // The remainder of in will be part of the next frame. + framePlain.add(in.readSlice(unprotectedBytesLeft)); + unprotectedBytesLeft = 0; + } + } + crypter.encrypt(frameOut, framePlain); + verify(!frameOut.isWritable()); + } + protectedBuf.readerIndex(0); + protectedBuf.writerIndex(protectedBuf.capacity()); + return protectedBuf.retain(); + } finally { + protectedBuf.release(); + } + } + } + + static final class Unprotector { + private final int suffixBytes; + private final ChannelCrypterNetty crypter; + + private DeframerState state = DeframerState.READ_HEADER; + private int requiredProtectedBytes; + private ByteBuf header; + private ByteBuf firstFrameTag; + private int unhandledIdx = 0; + private long unhandledBytes = 0; + private List unhandledBufs = new ArrayList<>(16); + + Unprotector(ChannelCrypterNetty crypter, ByteBufAllocator alloc) { + this.crypter = crypter; + this.suffixBytes = crypter.getSuffixLength(); + this.header = alloc.directBuffer(HEADER_BYTES); + this.firstFrameTag = alloc.directBuffer(suffixBytes); + } + + private void addUnhandled(ByteBuf in) { + if (in.isReadable()) { + ByteBuf buf = in.readRetainedSlice(in.readableBytes()); + unhandledBufs.add(buf); + unhandledBytes += buf.readableBytes(); + } + } + + void unprotect(ByteBuf in, List out, ByteBufAllocator alloc) + throws GeneralSecurityException { + checkState(header != null, "Cannot unprotect after destroy."); + addUnhandled(in); + decodeFrame(alloc, out); + } + + @SuppressWarnings("fallthrough") + private void decodeFrame(ByteBufAllocator alloc, List out) + throws GeneralSecurityException { + switch (state) { + case READ_HEADER: + if (unhandledBytes < HEADER_BYTES) { + return; + } + handleHeader(); + // fall through + case READ_PROTECTED_PAYLOAD: + if (unhandledBytes < requiredProtectedBytes) { + return; + } + ByteBuf unprotectedBuf; + try { + unprotectedBuf = handlePayload(alloc); + } finally { + clearState(); + } + if (unprotectedBuf != null) { + out.add(unprotectedBuf); + } + break; + default: + throw new AssertionError("impossible enum value"); + } + } + + private void handleHeader() { + while (header.isWritable()) { + ByteBuf in = unhandledBufs.get(unhandledIdx); + int headerBytesToRead = Math.min(in.readableBytes(), header.writableBytes()); + header.writeBytes(in, headerBytesToRead); + unhandledBytes -= headerBytesToRead; + if (!in.isReadable()) { + unhandledIdx++; + } + } + requiredProtectedBytes = header.readIntLE() - HEADER_TYPE_FIELD_BYTES; + checkArgument( + requiredProtectedBytes >= suffixBytes, "Invalid header field: frame size too small"); + checkArgument( + requiredProtectedBytes <= LIMIT_MAX_ALLOWED_FRAME_SIZE - HEADER_BYTES, + "Invalid header field: frame size too large"); + int frameType = header.readIntLE(); + checkArgument(frameType == HEADER_TYPE_DEFAULT, "Invalid header field: frame type"); + state = DeframerState.READ_PROTECTED_PAYLOAD; + } + + private ByteBuf handlePayload(ByteBufAllocator alloc) throws GeneralSecurityException { + int requiredCiphertextBytes = requiredProtectedBytes - suffixBytes; + int firstFrameUnprotectedLen = requiredCiphertextBytes; + + // We get the ciphertexts of the first frame and copy over the tag into a single buffer. + List firstFrameCiphertext = new ArrayList<>(); + while (requiredCiphertextBytes > 0) { + ByteBuf buf = unhandledBufs.get(unhandledIdx); + if (buf.readableBytes() <= requiredCiphertextBytes) { + // We use the whole buffer. + firstFrameCiphertext.add(buf); + requiredCiphertextBytes -= buf.readableBytes(); + unhandledIdx++; + } else { + firstFrameCiphertext.add(buf.readSlice(requiredCiphertextBytes)); + requiredCiphertextBytes = 0; + } + } + int requiredSuffixBytes = suffixBytes; + while (true) { + ByteBuf buf = unhandledBufs.get(unhandledIdx); + if (buf.readableBytes() <= requiredSuffixBytes) { + // We use the whole buffer. + requiredSuffixBytes -= buf.readableBytes(); + firstFrameTag.writeBytes(buf); + if (requiredSuffixBytes == 0) { + break; + } + unhandledIdx++; + } else { + firstFrameTag.writeBytes(buf, requiredSuffixBytes); + break; + } + } + verify(unhandledIdx == unhandledBufs.size() - 1); + ByteBuf lastBuf = unhandledBufs.get(unhandledIdx); + + // We get the remaining ciphertexts and tags contained in the last buffer. + List ciphertextsAndTags = new ArrayList<>(); + List unprotectedLens = new ArrayList<>(); + long requiredUnprotectedBytesCompleteFrames = firstFrameUnprotectedLen; + while (lastBuf.readableBytes() >= HEADER_BYTES + suffixBytes) { + // Read frame size. + int frameSize = lastBuf.readIntLE(); + int payloadSize = frameSize - HEADER_TYPE_FIELD_BYTES - suffixBytes; + // Break and undo read if we don't have the complete frame yet. + if (lastBuf.readableBytes() < frameSize) { + lastBuf.readerIndex(lastBuf.readerIndex() - HEADER_LEN_FIELD_BYTES); + break; + } + // Check the type header. + checkArgument(lastBuf.readIntLE() == 6); + // Create a new frame (except for out buffer). + ciphertextsAndTags.add(lastBuf.readSlice(payloadSize + suffixBytes)); + // Update sizes for frame. + requiredUnprotectedBytesCompleteFrames += payloadSize; + unprotectedLens.add(payloadSize); + } + + // We leave space for suffixBytes to allow for in-place encryption. This allows for calling + // doFinal in the JCE implementation which can be optimized better than update and doFinal. + ByteBuf unprotectedBuf = + alloc.directBuffer( + Ints.checkedCast(requiredUnprotectedBytesCompleteFrames + suffixBytes)); + try { + + ByteBuf out = writeSlice(unprotectedBuf, firstFrameUnprotectedLen + suffixBytes); + crypter.decrypt(out, firstFrameTag, firstFrameCiphertext); + verify(out.writableBytes() == suffixBytes); + unprotectedBuf.writerIndex(unprotectedBuf.writerIndex() - suffixBytes); + + for (int frameIdx = 0; frameIdx < ciphertextsAndTags.size(); ++frameIdx) { + out = writeSlice(unprotectedBuf, unprotectedLens.get(frameIdx) + suffixBytes); + crypter.decrypt(out, ciphertextsAndTags.get(frameIdx)); + verify(out.writableBytes() == suffixBytes); + unprotectedBuf.writerIndex(unprotectedBuf.writerIndex() - suffixBytes); + } + return unprotectedBuf.retain(); + } finally { + unprotectedBuf.release(); + } + } + + private void clearState() { + int bufsSize = unhandledBufs.size(); + ByteBuf lastBuf = unhandledBufs.get(bufsSize - 1); + boolean keepLast = lastBuf.isReadable(); + for (int bufIdx = 0; bufIdx < (keepLast ? bufsSize - 1 : bufsSize); ++bufIdx) { + unhandledBufs.get(bufIdx).release(); + } + unhandledBufs.clear(); + unhandledBytes = 0; + unhandledIdx = 0; + if (keepLast) { + unhandledBufs.add(lastBuf); + unhandledBytes = lastBuf.readableBytes(); + } + state = DeframerState.READ_HEADER; + requiredProtectedBytes = 0; + header.clear(); + firstFrameTag.clear(); + } + + void destroy() { + for (ByteBuf unhandledBuf : unhandledBufs) { + unhandledBuf.release(); + } + unhandledBufs.clear(); + if (header != null) { + header.release(); + header = null; + } + if (firstFrameTag != null) { + firstFrameTag.release(); + firstFrameTag = null; + } + crypter.destroy(); + } + } + + private enum DeframerState { + READ_HEADER, + READ_PROTECTED_PAYLOAD + } + + private static ByteBuf writeSlice(ByteBuf in, int len) { + checkArgument(len <= in.writableBytes()); + ByteBuf out = in.slice(in.writerIndex(), len); + in.writerIndex(in.writerIndex() + len); + return out.writerIndex(0); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java b/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java new file mode 100644 index 00000000000..2d6c322c1b1 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java @@ -0,0 +1,223 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import io.grpc.ChannelLogger; +import io.grpc.ChannelLogger.ChannelLogLevel; +import io.grpc.alts.internal.HandshakerServiceGrpc.HandshakerServiceStub; +import io.netty.buffer.ByteBufAllocator; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; + +/** + * Negotiates a grpc channel key to be used by the TsiFrameProtector, using ALTs handshaker service. + */ +public final class AltsTsiHandshaker implements TsiHandshaker { + private final ChannelLogger logger; + + public static final String TSI_SERVICE_ACCOUNT_PEER_PROPERTY = "service_account"; + + private final boolean isClient; + private final AltsHandshakerClient handshaker; + + private ByteBuffer outputFrame; + + /** Starts a new TSI handshaker with client options. */ + private AltsTsiHandshaker( + boolean isClient, + HandshakerServiceStub stub, + AltsHandshakerOptions options, + ChannelLogger logger) { + this.isClient = isClient; + this.logger = logger; + handshaker = new AltsHandshakerClient(stub, options, logger); + } + + @VisibleForTesting + AltsTsiHandshaker(boolean isClient, AltsHandshakerClient handshaker, ChannelLogger logger) { + this.isClient = isClient; + this.handshaker = handshaker; + this.logger = logger; + } + + /** + * Process the bytes received from the peer. + * + * @param bytes The buffer containing the handshake bytes from the peer. + * @return true, if the handshake has all the data it needs to process and false, if the method + * must be called again to complete processing. + */ + @Override + public boolean processBytesFromPeer(ByteBuffer bytes) throws GeneralSecurityException { + // If we're the client and we haven't given an output frame, we shouldn't be processing any + // bytes. + if (outputFrame == null && isClient) { + return true; + } + // If we already have bytes to write, just return. + if (outputFrame != null && outputFrame.hasRemaining()) { + return true; + } + int remaining = bytes.remaining(); + // Call handshaker service to process the bytes. + if (outputFrame == null) { + checkState(!isClient, "Client handshaker should not process any frame at the beginning."); + outputFrame = handshaker.startServerHandshake(bytes); + } else { + logger.log(ChannelLogLevel.DEBUG, "Receive ALTS handshake from downstream"); + outputFrame = handshaker.next(bytes); + } + // If handshake has finished or we already have bytes to write, just return true. + if (handshaker.isFinished() || outputFrame.hasRemaining()) { + return true; + } + // We have done processing input bytes, but no bytes to write. Thus we need more data. + if (!bytes.hasRemaining()) { + return false; + } + // There are still remaining bytes. Thus we need to continue processing the bytes. + // Prevent infinite loop by checking some bytes are consumed by handshaker. + checkState(bytes.remaining() < remaining, "Handshaker did not consume any bytes."); + return processBytesFromPeer(bytes); + } + + /** + * Returns the peer extracted from a completed handshake. + * + * @return the extracted peer. + */ + @Override + public TsiPeer extractPeer() throws GeneralSecurityException { + Preconditions.checkState(!isInProgress(), "Handshake is not complete."); + List> peerProperties = new ArrayList<>(); + peerProperties.add( + new TsiPeer.StringProperty( + TSI_SERVICE_ACCOUNT_PEER_PROPERTY, + handshaker.getResult().getPeerIdentity().getServiceAccount())); + return new TsiPeer(peerProperties); + } + + /** + * Returns the peer extracted from a completed handshake. + * + * @return the extracted peer. + */ + @Override + public Object extractPeerObject() throws GeneralSecurityException { + Preconditions.checkState(!isInProgress(), "Handshake is not complete."); + return new AltsInternalContext(handshaker.getResult()); + } + + /** Creates a new TsiHandshaker for use by the client. */ + public static TsiHandshaker newClient( + HandshakerServiceStub stub, AltsHandshakerOptions options, ChannelLogger logger) { + return new AltsTsiHandshaker(true, stub, options, logger); + } + + /** Creates a new TsiHandshaker for use by the server. */ + public static TsiHandshaker newServer( + HandshakerServiceStub stub, AltsHandshakerOptions options, ChannelLogger logger) { + return new AltsTsiHandshaker(false, stub, options, logger); + } + + /** + * Gets bytes that need to be sent to the peer. + * + * @param bytes The buffer to put handshake bytes. + */ + @Override + public void getBytesToSendToPeer(ByteBuffer bytes) throws GeneralSecurityException { + if (outputFrame == null) { // A null outputFrame indicates we haven't started the handshake. + if (isClient) { + logger.log(ChannelLogLevel.DEBUG, "Initial ALTS handshake to downstream"); + outputFrame = handshaker.startClientHandshake(); + } else { + // The server needs bytes to process before it can start the handshake. + return; + } + } + logger.log(ChannelLogLevel.DEBUG, "Send ALTS request to downstream"); + // Write as many bytes as we are able. + ByteBuffer outputFrameAlias = outputFrame; + if (outputFrame.remaining() > bytes.remaining()) { + outputFrameAlias = outputFrame.duplicate(); + ((Buffer) outputFrameAlias).limit(outputFrameAlias.position() + bytes.remaining()); + } + bytes.put(outputFrameAlias); + ((Buffer) outputFrame).position(outputFrameAlias.position()); + } + + /** + * Returns true if and only if the handshake is still in progress. + * + * @return true, if the handshake is still in progress, false otherwise. + */ + @Override + public boolean isInProgress() { + return !handshaker.isFinished() || outputFrame.hasRemaining(); + } + + /** + * Creates a frame protector from a completed handshake. No other methods may be called after the + * frame protector is created. + * + * @param maxFrameSize the requested max frame size, the callee is free to ignore. + * @param alloc used for allocating ByteBufs. + * @return a new TsiFrameProtector. + */ + @Override + public TsiFrameProtector createFrameProtector(int maxFrameSize, ByteBufAllocator alloc) { + Preconditions.checkState(!isInProgress(), "Handshake is not complete."); + + byte[] key = handshaker.getKey(); + Preconditions.checkState(key.length == AltsChannelCrypter.getKeyLength(), "Bad key length."); + + // Frame size negotiation is not performed if the peer does not send max frame size (e.g. peer + // is gRPC Go or peer uses an old binary). + int peerMaxFrameSize = handshaker.getResult().getMaxFrameSize(); + if (peerMaxFrameSize != 0) { + maxFrameSize = Math.min(peerMaxFrameSize, AltsTsiFrameProtector.getMaxFrameSize()); + maxFrameSize = Math.max(AltsTsiFrameProtector.getMinFrameSize(), maxFrameSize); + } + logger.log(ChannelLogLevel.INFO, "Maximum frame size value is {0}.", maxFrameSize); + return new AltsTsiFrameProtector(maxFrameSize, new AltsChannelCrypter(key, isClient), alloc); + } + + /** + * Creates a frame protector from a completed handshake. No other methods may be called after the + * frame protector is created. + * + * @param alloc used for allocating ByteBufs. + * @return a new TsiFrameProtector. + */ + @Override + public TsiFrameProtector createFrameProtector(ByteBufAllocator alloc) { + return createFrameProtector(AltsTsiFrameProtector.getMinFrameSize(), alloc); + } + + @Override + public void close() { + handshaker.close(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java b/alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java new file mode 100644 index 00000000000..a8251c7fbd3 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import java.util.LinkedList; +import java.util.Queue; + +/** Provides a semaphore primitive, without blocking waiting on permits. */ +final class AsyncSemaphore { + private final Object lock = new Object(); + @SuppressWarnings("JdkObsolete") // LinkedList avoids high watermark memory issues + private final Queue queue = new LinkedList<>(); + @GuardedBy("lock") + private int permits; + + public AsyncSemaphore(int permits) { + this.permits = permits; + } + + public ChannelFuture acquire(ChannelHandlerContext ctx) { + synchronized (lock) { + if (permits > 0) { + permits--; + return ctx.newSucceededFuture(); + } + ChannelPromise promise = ctx.newPromise(); + queue.add(promise); + return promise; + } + } + + public void release() { + ChannelPromise next; + synchronized (lock) { + next = queue.poll(); + if (next == null) { + permits++; + return; + } + } + next.setSuccess(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/BufUnwrapper.java b/alts/src/main/java/io/grpc/alts/internal/BufUnwrapper.java new file mode 100644 index 00000000000..9934dd2ff55 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/BufUnwrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import java.nio.ByteBuffer; + +/** Unwraps {@link ByteBuf}s into {@link ByteBuffer}s. */ +final class BufUnwrapper implements AutoCloseable { + + private final ByteBuffer[] singleReadBuffer = new ByteBuffer[1]; + private final ByteBuffer[] singleWriteBuffer = new ByteBuffer[1]; + + /** + * Called to get access to the underlying NIO buffers for a {@link ByteBuf} that will be used for + * writing. + */ + ByteBuffer[] writableNioBuffers(ByteBuf buf) { + // Set the writer index to the capacity to guarantee that the returned NIO buffers will have + // the capacity available. + int readerIndex = buf.readerIndex(); + int writerIndex = buf.writerIndex(); + buf.readerIndex(writerIndex); + buf.writerIndex(buf.capacity()); + + try { + return nioBuffers(buf, singleWriteBuffer); + } finally { + // Restore the writer index before returning. + buf.readerIndex(readerIndex); + buf.writerIndex(writerIndex); + } + } + + /** + * Called to get access to the underlying NIO buffers for a {@link ByteBuf} that will be used for + * reading. + */ + ByteBuffer[] readableNioBuffers(ByteBuf buf) { + return nioBuffers(buf, singleReadBuffer); + } + + @Override + public void close() { + singleReadBuffer[0] = null; + singleWriteBuffer[0] = null; + } + + /** + * Optimized accessor for obtaining the underlying NIO buffers for a Netty {@link ByteBuf}. Based + * on code from Netty's {@code SslHandler}. This method returns NIO buffers that span the readable + * region of the {@link ByteBuf}. + */ + private static ByteBuffer[] nioBuffers(ByteBuf buf, ByteBuffer[] singleBuffer) { + // As CompositeByteBuf.nioBufferCount() can be expensive (as it needs to check all composed + // ByteBuf to calculate the count) we will just assume a CompositeByteBuf contains more than 1 + // ByteBuf. The worst that can happen is that we allocate an extra ByteBuffer[] in + // CompositeByteBuf.nioBuffers() which is better than walking the composed ByteBuf in most + // cases. + if (!(buf instanceof CompositeByteBuf) && buf.nioBufferCount() == 1) { + // We know its only backed by 1 ByteBuffer so use internalNioBuffer to keep object + // allocation to a minimum. + singleBuffer[0] = buf.internalNioBuffer(buf.readerIndex(), buf.readableBytes()); + return singleBuffer; + } + + return buf.nioBuffers(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/ChannelCrypterNetty.java b/alts/src/main/java/io/grpc/alts/internal/ChannelCrypterNetty.java new file mode 100644 index 00000000000..e2e7d4046fb --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/ChannelCrypterNetty.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import io.netty.buffer.ByteBuf; +import java.security.GeneralSecurityException; +import java.util.List; + +/** + * A {@code ChannelCrypterNetty} performs stateful encryption and decryption of independent input + * and output streams. Both decrypt and encrypt gather their input from a list of Netty {@link + * ByteBuf} instances. + * + *

Note that we provide implementations of this interface that provide integrity only and + * implementations that provide privacy and integrity. All methods should be thread-compatible. + */ +public interface ChannelCrypterNetty { + + /** + * Encrypt plaintext into output buffer. + * + * @param out the protected input will be written into this buffer. The buffer must be direct and + * have enough space to hold all input buffers and the tag. Encrypt does not take ownership of + * this buffer. + * @param plain the input buffers that should be protected. Encrypt does not modify or take + * ownership of these buffers. + */ + void encrypt(ByteBuf out, List plain) throws GeneralSecurityException; + + /** + * Decrypt ciphertext into the given output buffer and check tag. + * + * @param out the unprotected input will be written into this buffer. The buffer must be direct + * and have enough space to hold all ciphertext buffers and the tag, i.e., it must have + * additional space for the tag, even though this space will be unused in the final result. + * Decrypt does not take ownership of this buffer. + * @param tag the tag appended to the ciphertext. Decrypt does not modify or take ownership of + * this buffer. + * @param ciphertext the buffers that should be unprotected (excluding the tag). Decrypt does not + * modify or take ownership of these buffers. + */ + void decrypt(ByteBuf out, ByteBuf tag, List ciphertext) throws GeneralSecurityException; + + /** + * Decrypt ciphertext into the given output buffer and check tag. + * + * @param out the unprotected input will be written into this buffer. The buffer must be direct + * and have enough space to hold all ciphertext buffers and the tag, i.e., it must have + * additional space for the tag, even though this space will be unused in the final result. + * Decrypt does not take ownership of this buffer. + * @param ciphertextAndTag single buffer containing ciphertext and tag that should be unprotected. + * The buffer must be direct and either completely overlap with {@code out} or not overlap at + * all. + */ + void decrypt(ByteBuf out, ByteBuf ciphertextAndTag) throws GeneralSecurityException; + + /** Returns the length of the tag in bytes. */ + int getSuffixLength(); + + /** Must be called to release all associated resources (instance cannot be used afterwards). */ + void destroy(); +} diff --git a/alts/src/main/java/io/grpc/alts/internal/NettyTsiHandshaker.java b/alts/src/main/java/io/grpc/alts/internal/NettyTsiHandshaker.java new file mode 100644 index 00000000000..b91cfdad08c --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/NettyTsiHandshaker.java @@ -0,0 +1,156 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** + * A wrapper for a {@link io.grpc.alts.internal.TsiHandshaker} that accepts netty {@link ByteBuf}s. + */ +public final class NettyTsiHandshaker { + + private BufUnwrapper unwrapper = new BufUnwrapper(); + private final TsiHandshaker internalHandshaker; + + public NettyTsiHandshaker(TsiHandshaker handshaker) { + internalHandshaker = checkNotNull(handshaker); + } + + /** + * Gets data that is ready to be sent to the to the remote peer. This should be called in a loop + * until no bytes are written to the output buffer. + * + * @param out the buffer to receive the bytes. + */ + void getBytesToSendToPeer(ByteBuf out) throws GeneralSecurityException { + checkState(unwrapper != null, "protector already created"); + try (BufUnwrapper unwrapper = this.unwrapper) { + // Write as many bytes as possible into the buffer. + int bytesWritten = 0; + for (ByteBuffer nioBuffer : unwrapper.writableNioBuffers(out)) { + if (!nioBuffer.hasRemaining()) { + // This buffer doesn't have any more space to write, go to the next buffer. + continue; + } + + int prevPos = nioBuffer.position(); + internalHandshaker.getBytesToSendToPeer(nioBuffer); + bytesWritten += nioBuffer.position() - prevPos; + + // If the buffer position was not changed, the frame has been completely read into the + // buffers. + if (nioBuffer.position() == prevPos) { + break; + } + } + + out.writerIndex(out.writerIndex() + bytesWritten); + } + } + + /** + * Process handshake data received from the remote peer. + * + * @return {@code true}, if the handshake has all the data it needs to process and {@code false}, + * if the method must be called again to complete processing. + */ + boolean processBytesFromPeer(ByteBuf data) throws GeneralSecurityException { + checkState(unwrapper != null, "protector already created"); + try (BufUnwrapper unwrapper = this.unwrapper) { + int bytesRead = 0; + boolean done = false; + for (ByteBuffer nioBuffer : unwrapper.readableNioBuffers(data)) { + if (!nioBuffer.hasRemaining()) { + // This buffer has been fully read, continue to the next buffer. + continue; + } + + int prevPos = nioBuffer.position(); + done = internalHandshaker.processBytesFromPeer(nioBuffer); + bytesRead += nioBuffer.position() - prevPos; + if (done) { + break; + } + } + + data.readerIndex(data.readerIndex() + bytesRead); + return done; + } + } + + /** + * Returns true if and only if the handshake is still in progress. + * + * @return true, if the handshake is still in progress, false otherwise. + */ + boolean isInProgress() { + return internalHandshaker.isInProgress(); + } + + /** + * Returns the peer extracted from a completed handshake. + * + * @return the extracted peer. + */ + TsiPeer extractPeer() throws GeneralSecurityException { + checkState(!internalHandshaker.isInProgress()); + return internalHandshaker.extractPeer(); + } + + /** + * Returns the peer extracted from a completed handshake. + * + * @return the extracted peer. + */ + Object extractPeerObject() throws GeneralSecurityException { + checkState(!internalHandshaker.isInProgress()); + return internalHandshaker.extractPeerObject(); + } + + /** + * Creates a frame protector from a completed handshake. No other methods may be called after the + * frame protector is created. + * + * @param maxFrameSize the requested max frame size, the callee is free to ignore. + * @return a new {@link io.grpc.alts.internal.TsiFrameProtector}. + */ + TsiFrameProtector createFrameProtector(int maxFrameSize, ByteBufAllocator alloc) { + unwrapper = null; + return internalHandshaker.createFrameProtector(maxFrameSize, alloc); + } + + /** + * Creates a frame protector from a completed handshake. No other methods may be called after the + * frame protector is created. + * + * @return a new {@link io.grpc.alts.internal.TsiFrameProtector}. + */ + TsiFrameProtector createFrameProtector(ByteBufAllocator alloc) { + unwrapper = null; + return internalHandshaker.createFrameProtector(alloc); + } + + void close() { + internalHandshaker.close(); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/ProtectedPromise.java b/alts/src/main/java/io/grpc/alts/internal/ProtectedPromise.java new file mode 100644 index 00000000000..871a51f1bea --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/ProtectedPromise.java @@ -0,0 +1,151 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkState; + +import io.grpc.Internal; +import io.netty.channel.Channel; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelPromise; +import io.netty.util.concurrent.EventExecutor; +import java.util.ArrayList; +import java.util.List; + +/** + * Promise used when flushing the {@code pendingUnprotectedWrites} queue. It manages the many-to + * many relationship between pending unprotected messages and the individual writes. Each protected + * frame will be written using the same instance of this promise and it will accumulate the results. + * Once all frames have been successfully written (or any failed), all of the promises for the + * pending unprotected writes are notified. + * + *

NOTE: this code is based on code in Netty's {@code Http2CodecUtil}. + */ +@Internal +public final class ProtectedPromise extends DefaultChannelPromise { + private final List unprotectedPromises; + private int expectedCount; + private int successfulCount; + private int failureCount; + private boolean doneAllocating; + + public ProtectedPromise(Channel channel, EventExecutor executor, int numUnprotectedPromises) { + super(channel, executor); + unprotectedPromises = new ArrayList<>(numUnprotectedPromises); + } + + /** + * Adds a promise for a pending unprotected write. This will be notified after all of the writes + * complete. + */ + public void addUnprotectedPromise(ChannelPromise promise) { + unprotectedPromises.add(promise); + } + + /** + * Allocate a new promise for the write of a protected frame. This will be used to aggregate the + * overall success of the unprotected promises. + * + * @return {@code this} promise. + */ + public ChannelPromise newPromise() { + checkState(!doneAllocating, "Done allocating. No more promises can be allocated."); + expectedCount++; + return this; + } + + /** + * Signify that no more {@link #newPromise()} allocations will be made. The aggregation can not be + * successful until this method is called. + * + * @return {@code this} promise. + */ + public ChannelPromise doneAllocatingPromises() { + if (!doneAllocating) { + doneAllocating = true; + if (successfulCount == expectedCount) { + trySuccessInternal(); + return super.setSuccess(null); + } + } + return this; + } + + @Override + public boolean tryFailure(Throwable cause) { + if (awaitingPromises()) { + ++failureCount; + if (failureCount == 1) { + tryFailureInternal(cause); + return super.tryFailure(cause); + } + // TODO: We break the interface a bit here. + // Multiple failure events can be processed without issue because this is an aggregation. + return true; + } + return false; + } + + /** + * Fail this object if it has not already been failed. + * + *

This method will NOT throw an {@link IllegalStateException} if called multiple times because + * that may be expected. + */ + @Override + public ChannelPromise setFailure(Throwable cause) { + tryFailure(cause); + return this; + } + + private boolean awaitingPromises() { + return successfulCount + failureCount < expectedCount; + } + + @Override + public ChannelPromise setSuccess(Void unused) { + trySuccess(null); + return this; + } + + @Override + public boolean trySuccess(Void unused) { + if (awaitingPromises()) { + ++successfulCount; + if (successfulCount == expectedCount && doneAllocating) { + trySuccessInternal(); + return super.trySuccess(null); + } + // TODO: We break the interface a bit here. + // Multiple success events can be processed without issue because this is an aggregation. + return true; + } + return false; + } + + private void trySuccessInternal() { + for (int i = 0; i < unprotectedPromises.size(); ++i) { + unprotectedPromises.get(i).trySuccess(null); + } + } + + private void tryFailureInternal(Throwable cause) { + for (int i = 0; i < unprotectedPromises.size(); ++i) { + unprotectedPromises.get(i).tryFailure(cause); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/RpcProtocolVersionsUtil.java b/alts/src/main/java/io/grpc/alts/internal/RpcProtocolVersionsUtil.java new file mode 100644 index 00000000000..005065b4e2e --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/RpcProtocolVersionsUtil.java @@ -0,0 +1,129 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.alts.internal.RpcProtocolVersions.Version; +import javax.annotation.Nullable; + +/** Utility class for Rpc Protocol Versions. */ +public final class RpcProtocolVersionsUtil { + + private static final int MAX_RPC_VERSION_MAJOR = 2; + private static final int MAX_RPC_VERSION_MINOR = 1; + private static final int MIN_RPC_VERSION_MAJOR = 2; + private static final int MIN_RPC_VERSION_MINOR = 1; + private static final RpcProtocolVersions RPC_PROTOCOL_VERSIONS = + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion( + RpcProtocolVersions.Version.newBuilder() + .setMajor(MAX_RPC_VERSION_MAJOR) + .setMinor(MAX_RPC_VERSION_MINOR) + .build()) + .setMinRpcVersion( + RpcProtocolVersions.Version.newBuilder() + .setMajor(MIN_RPC_VERSION_MAJOR) + .setMinor(MIN_RPC_VERSION_MINOR) + .build()) + .build(); + + /** Returns default Rpc Protocol Versions. */ + public static RpcProtocolVersions getRpcProtocolVersions() { + return RPC_PROTOCOL_VERSIONS; + } + + /** + * Returns true if first Rpc Protocol Version is greater than or equal to the second one. Returns + * false otherwise. + */ + @VisibleForTesting + static boolean isGreaterThanOrEqualTo(Version first, Version second) { + if ((first.getMajor() > second.getMajor()) + || (first.getMajor() == second.getMajor() && first.getMinor() >= second.getMinor())) { + return true; + } + return false; + } + + /** + * Performs check between local and peer Rpc Protocol Versions. This function returns true and the + * highest common version if there exists a common Rpc Protocol Version to use, and returns false + * and null otherwise. + */ + static RpcVersionsCheckResult checkRpcProtocolVersions( + RpcProtocolVersions localVersions, RpcProtocolVersions peerVersions) { + Version maxCommonVersion; + Version minCommonVersion; + // maxCommonVersion is MIN(local.max, peer.max) + if (isGreaterThanOrEqualTo(localVersions.getMaxRpcVersion(), peerVersions.getMaxRpcVersion())) { + maxCommonVersion = peerVersions.getMaxRpcVersion(); + } else { + maxCommonVersion = localVersions.getMaxRpcVersion(); + } + // minCommonVersion is MAX(local.min, peer.min) + if (isGreaterThanOrEqualTo(localVersions.getMinRpcVersion(), peerVersions.getMinRpcVersion())) { + minCommonVersion = localVersions.getMinRpcVersion(); + } else { + minCommonVersion = peerVersions.getMinRpcVersion(); + } + if (isGreaterThanOrEqualTo(maxCommonVersion, minCommonVersion)) { + return new RpcVersionsCheckResult.Builder() + .setResult(true) + .setHighestCommonVersion(maxCommonVersion) + .build(); + } + return new RpcVersionsCheckResult.Builder().setResult(false).build(); + } + + /** Wrapper class that stores results of Rpc Protocol Versions check. */ + static final class RpcVersionsCheckResult { + private final boolean result; + @Nullable private final Version highestCommonVersion; + + private RpcVersionsCheckResult(Builder builder) { + result = builder.result; + highestCommonVersion = builder.highestCommonVersion; + } + + boolean getResult() { + return result; + } + + Version getHighestCommonVersion() { + return highestCommonVersion; + } + + static final class Builder { + private boolean result; + @Nullable private Version highestCommonVersion = null; + + public Builder setResult(boolean result) { + this.result = result; + return this; + } + + public Builder setHighestCommonVersion(Version highestCommonVersion) { + this.highestCommonVersion = highestCommonVersion; + return this; + } + + public RpcVersionsCheckResult build() { + return new RpcVersionsCheckResult(this); + } + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/TsiFrameHandler.java b/alts/src/main/java/io/grpc/alts/internal/TsiFrameHandler.java new file mode 100644 index 00000000000..2294d7a07e6 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/TsiFrameHandler.java @@ -0,0 +1,199 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import io.grpc.alts.internal.TsiFrameProtector.Consumer; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.channel.PendingWriteQueue; +import io.netty.handler.codec.ByteToMessageDecoder; +import java.net.SocketAddress; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Encrypts and decrypts TSI Frames. Writes are buffered here until {@link #flush} is called. Writes + * must not be made before the TSI handshake is complete. + */ +public final class TsiFrameHandler extends ByteToMessageDecoder implements ChannelOutboundHandler { + + private static final Logger logger = Logger.getLogger(TsiFrameHandler.class.getName()); + + private TsiFrameProtector protector; + private PendingWriteQueue pendingUnprotectedWrites; + private boolean closeInitiated; + + public TsiFrameHandler(TsiFrameProtector protector) { + this.protector = checkNotNull(protector, "protector"); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + assert pendingUnprotectedWrites == null; + pendingUnprotectedWrites = new PendingWriteQueue(checkNotNull(ctx)); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + checkState(protector != null, "decode() called after close()"); + protector.unprotect(in, out, ctx.alloc()); + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") // for setSuccess + public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) { + if (protector == null) { + promise.setFailure(new IllegalStateException("write() called after close()")); + return; + } + ByteBuf msg = (ByteBuf) message; + if (!msg.isReadable()) { + // Nothing to encode. + promise.setSuccess(); + return; + } + + // Just add the message to the pending queue. We'll write it on the next flush. + pendingUnprotectedWrites.add(msg, promise); + } + + @Override + public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { + destroyProtectorAndWrites(); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + doClose(ctx); + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + doClose(ctx); + ctx.close(promise); + } + + private void doClose(ChannelHandlerContext ctx) { + if (closeInitiated) { + return; + } + closeInitiated = true; + try { + // flush any remaining writes before close + if (!pendingUnprotectedWrites.isEmpty()) { + flush(ctx); + } + } catch (GeneralSecurityException e) { + logger.log(Level.FINE, "Ignored error on flush before close", e); + } finally { + destroyProtectorAndWrites(); + } + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") // for aggregatePromise.doneAllocatingPromises + public void flush(final ChannelHandlerContext ctx) throws GeneralSecurityException { + if (pendingUnprotectedWrites == null || pendingUnprotectedWrites.isEmpty()) { + // Return early if there's nothing to write. Otherwise protector.protectFlush() below may + // not check for "no-data" and go on writing the 0-byte "data" to the socket with the + // protection framing. + return; + } + // Flushes can happen after close, but only when there are no pending writes. + checkState(protector != null, "flush() called after close()"); + final ProtectedPromise aggregatePromise = + new ProtectedPromise(ctx.channel(), ctx.executor(), pendingUnprotectedWrites.size()); + List bufs = new ArrayList<>(pendingUnprotectedWrites.size()); + + // Drain the unprotected writes. + while (!pendingUnprotectedWrites.isEmpty()) { + ByteBuf in = (ByteBuf) pendingUnprotectedWrites.current(); + bufs.add(in.retain()); + // Remove and release the buffer and add its promise to the aggregate. + aggregatePromise.addUnprotectedPromise(pendingUnprotectedWrites.remove()); + } + + final class ProtectedFrameWriteFlusher implements Consumer { + + @Override + public void accept(ByteBuf byteBuf) { + ctx.writeAndFlush(byteBuf, aggregatePromise.newPromise()); + } + } + + protector.protectFlush(bufs, new ProtectedFrameWriteFlusher(), ctx.alloc()); + // We're done writing, start the flow of promise events. + aggregatePromise.doneAllocatingPromises(); + } + + // Only here to fulfill ChannelOutboundHandler + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + // Only here to fulfill ChannelOutboundHandler + @Override + public void connect( + ChannelHandlerContext ctx, + SocketAddress remoteAddress, + SocketAddress localAddress, + ChannelPromise promise) { + ctx.connect(remoteAddress, localAddress, promise); + } + + // Only here to fulfill ChannelOutboundHandler + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.deregister(promise); + } + + // Only here to fulfill ChannelOutboundHandler + @Override + public void read(ChannelHandlerContext ctx) { + ctx.read(); + } + + private void destroyProtectorAndWrites() { + try { + if (pendingUnprotectedWrites != null && !pendingUnprotectedWrites.isEmpty()) { + pendingUnprotectedWrites.removeAndFailAll( + new ChannelException("Pending write on teardown of TSI handler")); + } + } finally { + pendingUnprotectedWrites = null; + } + if (protector != null) { + try { + protector.destroy(); + } finally { + protector = null; + } + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/TsiFrameProtector.java b/alts/src/main/java/io/grpc/alts/internal/TsiFrameProtector.java new file mode 100644 index 00000000000..b4227989ef2 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/TsiFrameProtector.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.security.GeneralSecurityException; +import java.util.List; + +/** + * This object protects and unprotects netty buffers once the handshake is done. + * + *

Implementations of this object must be thread compatible. + */ +public interface TsiFrameProtector { + + /** + * Protects the buffers by performing framing and encrypting/appending MACs. + * + * @param unprotectedBufs contain the payload that will be protected + * @param ctxWrite is called with buffers containing protected frames and must release the given + * buffers + * @param alloc is used to allocate new buffers for the protected frames + */ + void protectFlush( + List unprotectedBufs, Consumer ctxWrite, ByteBufAllocator alloc) + throws GeneralSecurityException; + + /** + * Unprotects the buffers by removing the framing and decrypting/checking MACs. + * + * @param in contains (partial) protected frames + * @param out is only used to append unprotected payload buffers + * @param alloc is used to allocate new buffers for the unprotected frames + */ + void unprotect(ByteBuf in, List out, ByteBufAllocator alloc) + throws GeneralSecurityException; + + /** Must be called to release all associated resources (instance cannot be used afterwards). */ + void destroy(); + + /** A mirror of java.util.function.Consumer without the Java 8 dependency. */ + interface Consumer { + void accept(T t); + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/TsiHandshakeHandler.java b/alts/src/main/java/io/grpc/alts/internal/TsiHandshakeHandler.java new file mode 100644 index 00000000000..7964b122f8c --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/TsiHandshakeHandler.java @@ -0,0 +1,250 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.grpc.alts.internal.AltsProtocolNegotiator.AUTH_CONTEXT_KEY; +import static io.grpc.alts.internal.AltsProtocolNegotiator.TSI_PEER_KEY; + +import io.grpc.Attributes; +import io.grpc.ChannelLogger; +import io.grpc.ChannelLogger.ChannelLogLevel; +import io.grpc.InternalChannelz.Security; +import io.grpc.SecurityLevel; +import io.grpc.alts.internal.TsiHandshakeHandler.HandshakeValidator.SecurityDetails; +import io.grpc.internal.GrpcAttributes; +import io.grpc.netty.InternalProtocolNegotiationEvent; +import io.grpc.netty.ProtocolNegotiationEvent; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import java.security.GeneralSecurityException; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Performs The TSI Handshake. + */ +public final class TsiHandshakeHandler extends ByteToMessageDecoder { + /** + * Validates a Tsi Peer object. + */ + public abstract static class HandshakeValidator { + + public static final class SecurityDetails { + + private final SecurityLevel securityLevel; + private final Security security; + + /** + * Constructs SecurityDetails. + */ + public SecurityDetails(io.grpc.SecurityLevel securityLevel, @Nullable Security security) { + this.securityLevel = checkNotNull(securityLevel, "securityLevel"); + this.security = security; + } + + public Security getSecurity() { + return security; + } + + public SecurityLevel getSecurityLevel() { + return securityLevel; + } + } + + /** + * Validates a Tsi Peer object. + */ + public abstract SecurityDetails validatePeerObject(Object peerObject) + throws GeneralSecurityException; + } + + private static final int HANDSHAKE_FRAME_SIZE = 1024; + + private final NettyTsiHandshaker handshaker; + private final HandshakeValidator handshakeValidator; + private final ChannelHandler next; + private final AsyncSemaphore semaphore; + + private ProtocolNegotiationEvent pne; + private boolean semaphoreAcquired; + private final ChannelLogger negotiationLogger; + + /** + * Constructs a TsiHandshakeHandler. + */ + public TsiHandshakeHandler( + ChannelHandler next, NettyTsiHandshaker handshaker, HandshakeValidator handshakeValidator, + ChannelLogger negotiationLogger) { + this(next, handshaker, handshakeValidator, null, negotiationLogger); + } + + /** + * Constructs a TsHandshakeHandler. If a semaphore is provided, a permit from the semaphore is + * required to start the handshake and is returned when the handshake ends. + */ + public TsiHandshakeHandler( + ChannelHandler next, NettyTsiHandshaker handshaker, HandshakeValidator handshakeValidator, + AsyncSemaphore semaphore, ChannelLogger negotiationLogger) { + this.handshaker = checkNotNull(handshaker, "handshaker"); + this.handshakeValidator = checkNotNull(handshakeValidator, "handshakeValidator"); + this.next = checkNotNull(next, "next"); + this.semaphore = semaphore; + this.negotiationLogger = negotiationLogger; + } + + @Override + protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out) + throws Exception { + // TODO: Not sure why override is needed. Investigate if it can be removed. + decode(ctx, in, out); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + // Process the data. If we need to send more data, do so now. + if (handshaker.processBytesFromPeer(in) && handshaker.isInProgress()) { + sendHandshake(ctx); + } + + // If the handshake is complete, transition to the framing state. + if (!handshaker.isInProgress()) { + TsiPeer peer = handshaker.extractPeer(); + Object authContext = handshaker.extractPeerObject(); + SecurityDetails details = handshakeValidator.validatePeerObject(authContext); + // createFrameProtector must be called last. + TsiFrameProtector protector = handshaker.createFrameProtector(ctx.alloc()); + TsiFrameHandler framer; + boolean success = false; + try { + framer = new TsiFrameHandler(protector); + // adding framer and next handler after this handler before removing Decoder (current + // handler). This will prevents any missing read from decoder and/or unframed write from + // next handler. + ctx.pipeline().addAfter(ctx.name(), null, framer); + ctx.pipeline().addAfter(ctx.pipeline().context(framer).name(), null, next); + ctx.pipeline().remove(ctx.name()); + fireProtocolNegotiationEvent(ctx, peer, authContext, details); + success = true; + } finally { + if (!success && protector != null) { + protector.destroy(); + } + } + } + } + + @Override + public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof ProtocolNegotiationEvent) { + checkState(pne == null, "negotiation already started"); + pne = (ProtocolNegotiationEvent) evt; + negotiationLogger.log(ChannelLogLevel.INFO, "TsiHandshake started"); + ChannelFuture acquire = semaphoreAcquire(ctx); + if (acquire.isSuccess()) { + semaphoreAcquired = true; + sendHandshake(ctx); + } else { + acquire.addListener(new ChannelFutureListener() { + @Override public void operationComplete(ChannelFuture future) { + if (!future.isSuccess()) { + ctx.fireExceptionCaught(future.cause()); + return; + } + if (ctx.isRemoved()) { + semaphoreRelease(); + return; + } + semaphoreAcquired = true; + try { + sendHandshake(ctx); + } catch (Exception ex) { + ctx.fireExceptionCaught(ex); + } + ctx.flush(); + } + }); + } + } else { + super.userEventTriggered(ctx, evt); + } + } + + private void fireProtocolNegotiationEvent( + ChannelHandlerContext ctx, TsiPeer peer, Object authContext, SecurityDetails details) { + checkState(pne != null, "negotiation not yet complete"); + negotiationLogger.log(ChannelLogLevel.INFO, "TsiHandshake finished"); + ProtocolNegotiationEvent localPne = pne; + Attributes.Builder attrs = InternalProtocolNegotiationEvent.getAttributes(localPne).toBuilder() + .set(TSI_PEER_KEY, peer) + .set(AUTH_CONTEXT_KEY, authContext) + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, details.getSecurityLevel()); + localPne = InternalProtocolNegotiationEvent.withAttributes(localPne, attrs.build()); + localPne = InternalProtocolNegotiationEvent.withSecurity(localPne, details.getSecurity()); + ctx.fireUserEventTriggered(localPne); + } + + /** Sends as many bytes as are available from the handshaker to the remote peer. */ + @SuppressWarnings("FutureReturnValueIgnored") // for addListener + private void sendHandshake(ChannelHandlerContext ctx) throws GeneralSecurityException { + while (true) { + boolean written = false; + ByteBuf buf = ctx.alloc().buffer(HANDSHAKE_FRAME_SIZE).retain(); // refcnt = 2 + try { + handshaker.getBytesToSendToPeer(buf); + if (buf.isReadable()) { + ctx.writeAndFlush(buf).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + written = true; + } else { + break; + } + } catch (GeneralSecurityException e) { + throw new GeneralSecurityException("TsiHandshakeHandler encountered exception", e); + } finally { + buf.release(written ? 1 : 2); + } + } + } + + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { + if (semaphoreAcquired) { + semaphoreRelease(); + semaphoreAcquired = false; + } + handshaker.close(); + } + + private ChannelFuture semaphoreAcquire(ChannelHandlerContext ctx) { + if (semaphore == null) { + return ctx.newSucceededFuture(); + } else { + return semaphore.acquire(ctx); + } + } + + private void semaphoreRelease() { + if (semaphore != null) { + semaphore.release(); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/internal/TsiHandshaker.java b/alts/src/main/java/io/grpc/alts/internal/TsiHandshaker.java new file mode 100644 index 00000000000..6580a4433c7 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/TsiHandshaker.java @@ -0,0 +1,114 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import io.netty.buffer.ByteBufAllocator; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** + * This object protects and unprotects buffers once the handshake is done. + * + *

A typical usage of this object would be: + * + *

{@code
+ * ByteBuffer buffer = allocateDirect(ALLOCATE_SIZE);
+ * while (true) {
+ *   while (true) {
+ *     tsiHandshaker.getBytesToSendToPeer(buffer.clear());
+ *     if (!buffer.hasRemaining()) break;
+ *     yourTransportSendMethod(buffer.flip());
+ *     assert(!buffer.hasRemaining());  // Guaranteed by yourTransportReceiveMethod(...)
+ *   }
+ *   if (!tsiHandshaker.isInProgress()) break;
+ *   while (true) {
+ *     assert(!buffer.hasRemaining());
+ *     yourTransportReceiveMethod(buffer.clear());
+ *     if (tsiHandshaker.processBytesFromPeer(buffer.flip())) break;
+ *   }
+ *   if (!tsiHandshaker.isInProgress()) break;
+ *   assert(!buffer.hasRemaining());
+ * }
+ * yourCheckPeerMethod(tsiHandshaker.extractPeer());
+ * TsiFrameProtector tsiFrameProtector = tsiHandshaker.createFrameProtector(MAX_FRAME_SIZE);
+ * if (buffer.hasRemaining()) tsiFrameProtector.unprotect(buffer, messageBuffer);
+ * }
+ * + *

Implementations of this object must be thread compatible. + */ +public interface TsiHandshaker { + /** + * Gets bytes that need to be sent to the peer. + * + * @param bytes The buffer to put handshake bytes. + */ + void getBytesToSendToPeer(ByteBuffer bytes) throws GeneralSecurityException; + + /** + * Process the bytes received from the peer. + * + * @param bytes The buffer containing the handshake bytes from the peer. + * @return true, if the handshake has all the data it needs to process and false, if the method + * must be called again to complete processing. + */ + boolean processBytesFromPeer(ByteBuffer bytes) throws GeneralSecurityException; + + /** + * Returns true if and only if the handshake is still in progress. + * + * @return true, if the handshake is still in progress, false otherwise. + */ + boolean isInProgress(); + + /** + * Returns the peer extracted from a completed handshake. + * + * @return the extracted peer. + */ + TsiPeer extractPeer() throws GeneralSecurityException; + + /** + * Returns the peer extracted from a completed handshake. + * + * @return the extracted peer. + */ + Object extractPeerObject() throws GeneralSecurityException; + + /** + * Creates a frame protector from a completed handshake. No other methods may be called after the + * frame protector is created. + * + * @param maxFrameSize the requested max frame size, the callee is free to ignore. + * @param alloc used for allocating ByteBufs. + * @return a new TsiFrameProtector. + */ + TsiFrameProtector createFrameProtector(int maxFrameSize, ByteBufAllocator alloc); + + /** + * Creates a frame protector from a completed handshake. No other methods may be called after the + * frame protector is created. + * + * @param alloc used for allocating ByteBufs. + * @return a new TsiFrameProtector. + */ + TsiFrameProtector createFrameProtector(ByteBufAllocator alloc); + + /** + * Closes resources. + */ + void close(); +} diff --git a/alts/src/main/java/io/grpc/alts/internal/TsiHandshakerFactory.java b/alts/src/main/java/io/grpc/alts/internal/TsiHandshakerFactory.java new file mode 100644 index 00000000000..7d17a3954c8 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/TsiHandshakerFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import io.grpc.ChannelLogger; +import javax.annotation.Nullable; + +/** Factory that manufactures instances of {@link TsiHandshaker}. */ +public interface TsiHandshakerFactory { + + /** Creates a new handshaker. */ + TsiHandshaker newHandshaker(@Nullable String authority, ChannelLogger logger); +} diff --git a/alts/src/main/java/io/grpc/alts/internal/TsiPeer.java b/alts/src/main/java/io/grpc/alts/internal/TsiPeer.java new file mode 100644 index 00000000000..c02188c50ac --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/TsiPeer.java @@ -0,0 +1,117 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; + +/** A set of peer properties. */ +public final class TsiPeer { + private final List> properties; + + public TsiPeer(List> properties) { + this.properties = Collections.unmodifiableList(properties); + } + + public List> getProperties() { + return properties; + } + + /** Get peer property. */ + public Property getProperty(String name) { + for (Property property : properties) { + if (property.getName().equals(name)) { + return property; + } + } + return null; + } + + @Override + public String toString() { + return new ArrayList<>(properties).toString(); + } + + /** A peer property. */ + public abstract static class Property { + private final String name; + private final T value; + + protected Property(@Nonnull String name, @Nonnull T value) { + this.name = name; + this.value = value; + } + + public final T getValue() { + return value; + } + + public final String getName() { + return name; + } + + @Override + public String toString() { + return String.format("%s=%s", name, value); + } + } + + /** A peer property corresponding to a boolean. */ + public static final class BooleanProperty extends Property { + public BooleanProperty(@Nonnull String name, boolean value) { + super(name, value); + } + } + + /** A peer property corresponding to a signed 64-bit integer. */ + public static final class SignedInt64Property extends Property { + public SignedInt64Property(@Nonnull String name, @Nonnull Long value) { + super(name, value); + } + } + + /** A peer property corresponding to an unsigned 64-bit integer. */ + public static final class UnsignedInt64Property extends Property { + public UnsignedInt64Property(@Nonnull String name, @Nonnull BigInteger value) { + super(name, value); + } + } + + /** A peer property corresponding to a double. */ + public static final class DoubleProperty extends Property { + public DoubleProperty(@Nonnull String name, @Nonnull Double value) { + super(name, value); + } + } + + /** A peer property corresponding to a string. */ + public static final class StringProperty extends Property { + public StringProperty(@Nonnull String name, @Nonnull String value) { + super(name, value); + } + } + + /** A peer property corresponding to a list of peer properties. */ + public static final class PropertyList extends Property>> { + public PropertyList(@Nonnull String name, @Nonnull List> value) { + super(name, value); + } + } +} diff --git a/alts/src/main/proto/grpc/gcp/altscontext.proto b/alts/src/main/proto/grpc/gcp/altscontext.proto new file mode 100644 index 00000000000..a6cceb1c303 --- /dev/null +++ b/alts/src/main/proto/grpc/gcp/altscontext.proto @@ -0,0 +1,50 @@ +// Copyright 2018 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/altscontext.proto + +syntax = "proto3"; + +package grpc.gcp; + +import "grpc/gcp/transport_security_common.proto"; + +option go_package = "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"; +option java_multiple_files = true; +option java_outer_classname = "AltsContextProto"; +option java_package = "io.grpc.alts.internal"; + +message AltsContext { + // The application protocol negotiated for this connection. + string application_protocol = 1; + + // The record protocol negotiated for this connection. + string record_protocol = 2; + + // The security level of the created secure channel. + SecurityLevel security_level = 3; + + // The peer service account. + string peer_service_account = 4; + + // The local service account. + string local_service_account = 5; + + // The RPC protocol versions supported by the peer. + RpcProtocolVersions peer_rpc_versions = 6; + + // Additional attributes of the peer. + map peer_attributes = 7; +} diff --git a/alts/src/main/proto/grpc/gcp/handshaker.proto b/alts/src/main/proto/grpc/gcp/handshaker.proto new file mode 100644 index 00000000000..02764ba4c08 --- /dev/null +++ b/alts/src/main/proto/grpc/gcp/handshaker.proto @@ -0,0 +1,243 @@ +// Copyright 2018 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/handshaker.proto + +syntax = "proto3"; + +package grpc.gcp; + +import "grpc/gcp/transport_security_common.proto"; + +option go_package = "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"; +option java_multiple_files = true; +option java_outer_classname = "HandshakerProto"; +option java_package = "io.grpc.alts.internal"; + + +enum HandshakeProtocol { + // Default value. + HANDSHAKE_PROTOCOL_UNSPECIFIED = 0; + + // TLS handshake protocol. + TLS = 1; + + // Application Layer Transport Security handshake protocol. + ALTS = 2; +} + +enum NetworkProtocol { + NETWORK_PROTOCOL_UNSPECIFIED = 0; + TCP = 1; + UDP = 2; +} + +message Endpoint { + // IP address. It should contain an IPv4 or IPv6 string literal, e.g. + // "192.168.0.1" or "2001:db8::1". + string ip_address = 1; + + // Port number. + int32 port = 2; + + // Network protocol (e.g., TCP, UDP) associated with this endpoint. + NetworkProtocol protocol = 3; +} + +message Identity { + oneof identity_oneof { + // Service account of a connection endpoint. + string service_account = 1; + + // Hostname of a connection endpoint. + string hostname = 2; + } + + // Additional attributes of the identity. + map attributes = 3; +} + +message StartClientHandshakeReq { + // Handshake security protocol requested by the client. + HandshakeProtocol handshake_security_protocol = 1; + + // The application protocols supported by the client, e.g., "h2" (for http2), + // "grpc". + repeated string application_protocols = 2; + + // The record protocols supported by the client, e.g., + // "ALTSRP_GCM_AES128". + repeated string record_protocols = 3; + + // (Optional) Describes which server identities are acceptable by the client. + // If target identities are provided and none of them matches the peer + // identity of the server, handshake will fail. + repeated Identity target_identities = 4; + + // (Optional) Application may specify a local identity. Otherwise, the + // handshaker chooses a default local identity. + Identity local_identity = 5; + + // (Optional) Local endpoint information of the connection to the server, + // such as local IP address, port number, and network protocol. + Endpoint local_endpoint = 6; + + // (Optional) Endpoint information of the remote server, such as IP address, + // port number, and network protocol. + Endpoint remote_endpoint = 7; + + // (Optional) If target name is provided, a secure naming check is performed + // to verify that the peer authenticated identity is indeed authorized to run + // the target name. + string target_name = 8; + + // (Optional) RPC protocol versions supported by the client. + RpcProtocolVersions rpc_versions = 9; + + // (Optional) Maximum frame size supported by the client. + uint32 max_frame_size = 10; +} + +message ServerHandshakeParameters { + // The record protocols supported by the server, e.g., + // "ALTSRP_GCM_AES128". + repeated string record_protocols = 1; + + // (Optional) A list of local identities supported by the server, if + // specified. Otherwise, the handshaker chooses a default local identity. + repeated Identity local_identities = 2; +} + +message StartServerHandshakeReq { + // The application protocols supported by the server, e.g., "h2" (for http2), + // "grpc". + repeated string application_protocols = 1; + + // Handshake parameters (record protocols and local identities supported by + // the server) mapped by the handshake protocol. Each handshake security + // protocol (e.g., TLS or ALTS) has its own set of record protocols and local + // identities. Since protobuf does not support enum as key to the map, the key + // to handshake_parameters is the integer value of HandshakeProtocol enum. + map handshake_parameters = 2; + + // Bytes in out_frames returned from the peer's HandshakerResp. It is possible + // that the peer's out_frames are split into multiple HandshakReq messages. + bytes in_bytes = 3; + + // (Optional) Local endpoint information of the connection to the client, + // such as local IP address, port number, and network protocol. + Endpoint local_endpoint = 4; + + // (Optional) Endpoint information of the remote client, such as IP address, + // port number, and network protocol. + Endpoint remote_endpoint = 5; + + // (Optional) RPC protocol versions supported by the server. + RpcProtocolVersions rpc_versions = 6; + + // (Optional) Maximum frame size supported by the server. + uint32 max_frame_size = 7; +} + +message NextHandshakeMessageReq { + // Bytes in out_frames returned from the peer's HandshakerResp. It is possible + // that the peer's out_frames are split into multiple NextHandshakerMessageReq + // messages. + bytes in_bytes = 1; +} + +message HandshakerReq { + oneof req_oneof { + // The start client handshake request message. + StartClientHandshakeReq client_start = 1; + + // The start server handshake request message. + StartServerHandshakeReq server_start = 2; + + // The next handshake request message. + NextHandshakeMessageReq next = 3; + } +} + +message HandshakerResult { + // The application protocol negotiated for this connection. + string application_protocol = 1; + + // The record protocol negotiated for this connection. + string record_protocol = 2; + + // Cryptographic key data. The key data may be more than the key length + // required for the record protocol, thus the client of the handshaker + // service needs to truncate the key data into the right key length. + bytes key_data = 3; + + // The authenticated identity of the peer. + Identity peer_identity = 4; + + // The local identity used in the handshake. + Identity local_identity = 5; + + // Indicate whether the handshaker service client should keep the channel + // between the handshaker service open, e.g., in order to handle + // post-handshake messages in the future. + bool keep_channel_open = 6; + + // The RPC protocol versions supported by the peer. + RpcProtocolVersions peer_rpc_versions = 7; + + // The maximum frame size of the peer. + uint32 max_frame_size = 8; +} + +message HandshakerStatus { + // The status code. This could be the gRPC status code. + uint32 code = 1; + + // The status details. + string details = 2; +} + +message HandshakerResp { + // Frames to be given to the peer for the NextHandshakeMessageReq. May be + // empty if no out_frames have to be sent to the peer or if in_bytes in the + // HandshakerReq are incomplete. All the non-empty out frames must be sent to + // the peer even if the handshaker status is not OK as these frames may + // contain the alert frames. + bytes out_frames = 1; + + // Number of bytes in the in_bytes consumed by the handshaker. It is possible + // that part of in_bytes in HandshakerReq was unrelated to the handshake + // process. + uint32 bytes_consumed = 2; + + // This is set iff the handshake was successful. out_frames may still be set + // to frames that needs to be forwarded to the peer. + HandshakerResult result = 3; + + // Status of the handshaker. + HandshakerStatus status = 4; +} + +service HandshakerService { + // Handshaker service accepts a stream of handshaker request, returning a + // stream of handshaker response. Client is expected to send exactly one + // message with either client_start or server_start followed by one or more + // messages with next. Each time client sends a request, the handshaker + // service expects to respond. Client does not have to wait for service's + // response before sending next request. + rpc DoHandshake(stream HandshakerReq) + returns (stream HandshakerResp) { + } +} diff --git a/alts/src/main/proto/grpc/gcp/transport_security_common.proto b/alts/src/main/proto/grpc/gcp/transport_security_common.proto new file mode 100644 index 00000000000..8f01be79e36 --- /dev/null +++ b/alts/src/main/proto/grpc/gcp/transport_security_common.proto @@ -0,0 +1,46 @@ +// Copyright 2018 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/transport_security_common.proto + +syntax = "proto3"; + +package grpc.gcp; + +option go_package = "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"; +option java_multiple_files = true; +option java_outer_classname = "TransportSecurityCommonProto"; +option java_package = "io.grpc.alts.internal"; + +// The security level of the created channel. The list is sorted in increasing +// level of security. This order must always be maintained. +enum SecurityLevel { + SECURITY_NONE = 0; + INTEGRITY_ONLY = 1; + INTEGRITY_AND_PRIVACY = 2; +} + +// Max and min supported RPC protocol versions. +message RpcProtocolVersions { + // RPC version contains a major version and a minor version. + message Version { + uint32 major = 1; + uint32 minor = 2; + } + // Maximum supported RPC version. + Version max_rpc_version = 1; + // Minimum supported RPC version. + Version min_rpc_version = 2; +} diff --git a/alts/src/test/java/io/grpc/alts/AltsChannelBuilderTest.java b/alts/src/test/java/io/grpc/alts/AltsChannelBuilderTest.java new file mode 100644 index 00000000000..a44de19b911 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/AltsChannelBuilderTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class AltsChannelBuilderTest { + + @Test + public void buildsNettyChannel() { + AltsChannelBuilder builder = + AltsChannelBuilder.forTarget("localhost:8080").enableUntrustedAltsForTesting(); + + ProtocolNegotiator protocolNegotiator = builder.getProtocolNegotiatorForTest(); + assertThat(protocolNegotiator).isNotNull(); + // Avoids exposing this class + assertThat(protocolNegotiator.getClass().getSimpleName()) + .isEqualTo("ClientAltsProtocolNegotiator"); + } +} diff --git a/alts/src/test/java/io/grpc/alts/AltsContextUtilTest.java b/alts/src/test/java/io/grpc/alts/AltsContextUtilTest.java new file mode 100644 index 00000000000..675fa29fc99 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/AltsContextUtilTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package io.grpc.alts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.grpc.Attributes; +import io.grpc.ClientCall; +import io.grpc.ServerCall; +import io.grpc.alts.AltsContext.SecurityLevel; +import io.grpc.alts.internal.AltsInternalContext; +import io.grpc.alts.internal.AltsProtocolNegotiator; +import io.grpc.alts.internal.HandshakerResult; +import io.grpc.alts.internal.Identity; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsContextUtil}. */ +@RunWith(JUnit4.class) +public class AltsContextUtilTest { + @Test + public void check_noAttributeValue() { + assertFalse(AltsContextUtil.check(Attributes.newBuilder().build())); + } + + @Test + public void check_unexpectedAttributeValueType() { + assertFalse(AltsContextUtil.check(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new Object()) + .build())); + } + + @Test + public void check_altsInternalContext() { + assertTrue(AltsContextUtil.check(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance()) + .build())); + } + + @Test + public void checkServer_altsInternalContext() { + ServerCall call = mock(ServerCall.class); + when(call.getAttributes()).thenReturn(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance()) + .build()); + + assertTrue(AltsContextUtil.check(call)); + } + + @Test + public void checkClient_altsInternalContext() { + ClientCall call = mock(ClientCall.class); + when(call.getAttributes()).thenReturn(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance()) + .build()); + + assertTrue(AltsContextUtil.check(call)); + } + + @Test + public void createFrom_altsInternalContext() { + HandshakerResult handshakerResult = + HandshakerResult.newBuilder() + .setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer")) + .setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer")) + .build(); + + AltsContext context = AltsContextUtil.createFrom(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult)) + .build()); + assertEquals("remote@peer", context.getPeerServiceAccount()); + assertEquals("local@peer", context.getLocalServiceAccount()); + assertEquals(SecurityLevel.INTEGRITY_AND_PRIVACY, context.getSecurityLevel()); + } + + @Test(expected = IllegalArgumentException.class) + public void createFrom_noAttributeValue() { + AltsContextUtil.createFrom(Attributes.newBuilder().build()); + } + + @Test + public void createFromServer_altsInternalContext() { + HandshakerResult handshakerResult = + HandshakerResult.newBuilder() + .setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer")) + .setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer")) + .build(); + + ServerCall call = mock(ServerCall.class); + when(call.getAttributes()).thenReturn(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult)) + .build()); + + AltsContext context = AltsContextUtil.createFrom(call); + assertEquals("remote@peer", context.getPeerServiceAccount()); + } + + @Test + public void createFromClient_altsInternalContext() { + HandshakerResult handshakerResult = + HandshakerResult.newBuilder() + .setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer")) + .setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer")) + .build(); + + ClientCall call = mock(ClientCall.class); + when(call.getAttributes()).thenReturn(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult)) + .build()); + + AltsContext context = AltsContextUtil.createFrom(call); + assertEquals("remote@peer", context.getPeerServiceAccount()); + } +} diff --git a/alts/src/test/java/io/grpc/alts/AltsServerBuilderTest.java b/alts/src/test/java/io/grpc/alts/AltsServerBuilderTest.java new file mode 100644 index 00000000000..f729a8be2b3 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/AltsServerBuilderTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class AltsServerBuilderTest { + + @Test + public void buildsNettyServer() throws Exception { + AltsServerBuilder.forPort(1234).enableUntrustedAltsForTesting().build(); + } +} diff --git a/alts/src/test/java/io/grpc/alts/AuthorizationUtilTest.java b/alts/src/test/java/io/grpc/alts/AuthorizationUtilTest.java new file mode 100644 index 00000000000..8b4691df331 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/AuthorizationUtilTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.Lists; +import io.grpc.Attributes; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.Status; +import io.grpc.alts.internal.AltsInternalContext; +import io.grpc.alts.internal.AltsProtocolNegotiator; +import io.grpc.alts.internal.HandshakerResult; +import io.grpc.alts.internal.Identity; +import javax.annotation.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AuthorizationUtil}. */ +@RunWith(JUnit4.class) +public final class AuthorizationUtilTest { + + @Test + public void altsAuthorizationCheck() throws Exception { + Status status = + AuthorizationUtil.clientAuthorizationCheck( + new FakeServerCall(null), Lists.newArrayList("Alice")); + assertThat(status.getCode()).isEqualTo(Status.Code.PERMISSION_DENIED); + assertThat(status.getDescription()).startsWith("Peer ALTS AuthContext not found"); + status = + AuthorizationUtil.clientAuthorizationCheck( + new FakeServerCall("Alice"), Lists.newArrayList("Alice", "Bob")); + assertThat(status.getCode()).isEqualTo(Status.Code.OK); + status = + AuthorizationUtil.clientAuthorizationCheck( + new FakeServerCall("Alice"), Lists.newArrayList("Bob", "Joe")); + assertThat(status.getCode()).isEqualTo(Status.Code.PERMISSION_DENIED); + assertThat(status.getDescription()).endsWith("not authorized"); + } + + private static class FakeServerCall extends ServerCall { + final Attributes attrs; + + FakeServerCall(@Nullable String peerServiceAccount) { + Attributes.Builder attrsBuilder = Attributes.newBuilder(); + if (peerServiceAccount != null) { + HandshakerResult handshakerResult = + HandshakerResult.newBuilder() + .setPeerIdentity(Identity.newBuilder().setServiceAccount(peerServiceAccount)) + .build(); + AltsInternalContext altsContext = new AltsInternalContext(handshakerResult); + attrsBuilder.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, altsContext); + } + attrs = attrsBuilder.build(); + } + + @Override + public void request(int numMessages) { + throw new AssertionError("Should not be called"); + } + + @Override + public void sendHeaders(Metadata headers) { + throw new AssertionError("Should not be called"); + } + + @Override + public void sendMessage(String message) { + throw new AssertionError("Should not be called"); + } + + @Override + public void close(Status status, Metadata trailers) { + throw new AssertionError("Should not be called"); + } + + @Override + public boolean isCancelled() { + throw new AssertionError("Should not be called"); + } + + @Override + public Attributes getAttributes() { + return attrs; + } + + @Override + public String getAuthority() { + throw new AssertionError("Should not be called"); + } + + @Override + public MethodDescriptor getMethodDescriptor() { + throw new AssertionError("Should not be called"); + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/ComputeEngineChannelBuilderTest.java b/alts/src/test/java/io/grpc/alts/ComputeEngineChannelBuilderTest.java new file mode 100644 index 00000000000..03fd113a65e --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/ComputeEngineChannelBuilderTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ComputeEngineChannelBuilderTest { + + @Test + public void buildsNettyChannel() throws Exception { + ComputeEngineChannelBuilder builder = ComputeEngineChannelBuilder.forTarget("localhost:8080"); + builder.build(); + } +} diff --git a/alts/src/test/java/io/grpc/alts/DualCallCredentialsTest.java b/alts/src/test/java/io/grpc/alts/DualCallCredentialsTest.java new file mode 100644 index 00000000000..29646191be1 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/DualCallCredentialsTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.grpc.Attributes; +import io.grpc.CallCredentials; +import io.grpc.CallCredentials.RequestInfo; +import io.grpc.MethodDescriptor; +import io.grpc.SecurityLevel; +import io.grpc.alts.internal.AltsInternalContext; +import io.grpc.alts.internal.AltsProtocolNegotiator; +import io.grpc.testing.TestMethodDescriptors; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link DualCallCredentials}. */ +@RunWith(JUnit4.class) +public class DualCallCredentialsTest { + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock CallCredentials tlsCallCredentials; + + @Mock CallCredentials altsCallCredentials; + + private static final String AUTHORITY = "testauthority"; + private static final SecurityLevel SECURITY_LEVEL = SecurityLevel.PRIVACY_AND_INTEGRITY; + + @Test + public void invokeTlsCallCredentials() { + DualCallCredentials callCredentials = + new DualCallCredentials(tlsCallCredentials, altsCallCredentials); + RequestInfo requestInfo = new RequestInfoImpl(false); + callCredentials.applyRequestMetadata(requestInfo, null, null); + + verify(altsCallCredentials, never()).applyRequestMetadata(any(), any(), any()); + verify(tlsCallCredentials, times(1)).applyRequestMetadata(requestInfo, null, null); + } + + @Test + public void invokeAltsCallCredentials() { + DualCallCredentials callCredentials = + new DualCallCredentials(tlsCallCredentials, altsCallCredentials); + RequestInfo requestInfo = new RequestInfoImpl(true); + callCredentials.applyRequestMetadata(requestInfo, null, null); + + verify(altsCallCredentials, times(1)).applyRequestMetadata(requestInfo, null, null); + verify(tlsCallCredentials, never()).applyRequestMetadata(any(), any(), any()); + } + + private static final class RequestInfoImpl extends CallCredentials.RequestInfo { + private Attributes attrs; + + RequestInfoImpl(boolean hasAltsContext) { + attrs = + hasAltsContext + ? Attributes.newBuilder() + .set( + AltsProtocolNegotiator.AUTH_CONTEXT_KEY, + AltsInternalContext.getDefaultInstance()) + .build() + : Attributes.EMPTY; + } + + @Override + public MethodDescriptor getMethodDescriptor() { + return TestMethodDescriptors.voidMethod(); + } + + @Override + public SecurityLevel getSecurityLevel() { + return SECURITY_LEVEL; + } + + @Override + public String getAuthority() { + return AUTHORITY; + } + + @Override + public Attributes getTransportAttrs() { + return attrs; + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/GoogleDefaultChannelBuilderTest.java b/alts/src/test/java/io/grpc/alts/GoogleDefaultChannelBuilderTest.java new file mode 100644 index 00000000000..c73ef4444e9 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/GoogleDefaultChannelBuilderTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class GoogleDefaultChannelBuilderTest { + + @Test + public void buildsNettyChannel() throws Exception { + GoogleDefaultChannelBuilder builder = GoogleDefaultChannelBuilder.forTarget("localhost:8080"); + builder.build(); + } +} diff --git a/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java b/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java new file mode 100644 index 00000000000..221001157f1 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.Channel; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.internal.SharedResourceHolder.Resource; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class HandshakerServiceChannelTest { + @Rule + public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private final Server server = grpcCleanup.register( + ServerBuilder.forPort(0) + .addService(new SimpleServiceGrpc.SimpleServiceImplBase() { + @Override + public void unaryRpc(SimpleRequest request, StreamObserver so) { + so.onNext(SimpleResponse.getDefaultInstance()); + so.onCompleted(); + } + }) + .build()); + private Resource resource; + + @Before + public void setUp() throws Exception { + server.start(); + resource = + HandshakerServiceChannel.getHandshakerChannelForTesting("localhost:" + server.getPort()); + } + + @Test + public void sharedChannel_authority() { + resource = HandshakerServiceChannel.SHARED_HANDSHAKER_CHANNEL; + Channel channel = resource.create(); + try { + assertThat(channel.authority()).isEqualTo("metadata.google.internal.:8080"); + } finally { + resource.close(channel); + } + } + + @Test + public void getHandshakerTarget_nullEnvVar() { + assertThat(HandshakerServiceChannel.getHandshakerTarget(null)) + .isEqualTo("metadata.google.internal.:8080"); + } + + @Test + public void getHandshakerTarget_envVarWithPort() { + assertThat(HandshakerServiceChannel.getHandshakerTarget("169.254.169.254:80")) + .isEqualTo("169.254.169.254:8080"); + } + + @Test + public void getHandshakerTarget_envVarWithHostOnly() { + assertThat(HandshakerServiceChannel.getHandshakerTarget("169.254.169.254")) + .isEqualTo("169.254.169.254:8080"); + } + + @Test + public void resource_works() { + Channel channel = resource.create(); + try { + // Do an RPC to verify that the channel actually works + doRpc(channel); + } finally { + resource.close(channel); + } + } + + @Test + public void resource_lifecycleTwice() { + Channel channel = resource.create(); + try { + doRpc(channel); + } finally { + resource.close(channel); + } + channel = resource.create(); + try { + doRpc(channel); + } finally { + resource.close(channel); + } + } + + private void doRpc(Channel channel) { + SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance()); + } +} diff --git a/alts/src/test/java/io/grpc/alts/InternalCheckGcpEnvironmentTest.java b/alts/src/test/java/io/grpc/alts/InternalCheckGcpEnvironmentTest.java new file mode 100644 index 00000000000..1234cfc49c3 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/InternalCheckGcpEnvironmentTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedReader; +import java.io.StringReader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class InternalCheckGcpEnvironmentTest { + + @Test + public void checkGcpLinuxPlatformData() throws Exception { + BufferedReader reader; + reader = new BufferedReader(new StringReader("HP Z440 Workstation")); + assertFalse(InternalCheckGcpEnvironment.checkProductNameOnLinux(reader)); + reader = new BufferedReader(new StringReader("Google")); + assertTrue(InternalCheckGcpEnvironment.checkProductNameOnLinux(reader)); + reader = new BufferedReader(new StringReader("Google Compute Engine")); + assertTrue(InternalCheckGcpEnvironment.checkProductNameOnLinux(reader)); + reader = new BufferedReader(new StringReader("Google Compute Engine ")); + assertTrue(InternalCheckGcpEnvironment.checkProductNameOnLinux(reader)); + } + + @Test + public void checkGcpWindowsPlatformData() throws Exception { + BufferedReader reader; + reader = new BufferedReader(new StringReader("Product : Google")); + assertFalse(InternalCheckGcpEnvironment.checkBiosDataOnWindows(reader)); + reader = new BufferedReader(new StringReader("Manufacturer : LENOVO")); + assertFalse(InternalCheckGcpEnvironment.checkBiosDataOnWindows(reader)); + reader = new BufferedReader(new StringReader("Manufacturer : Google Compute Engine")); + assertFalse(InternalCheckGcpEnvironment.checkBiosDataOnWindows(reader)); + reader = new BufferedReader(new StringReader("Manufacturer : Google")); + assertTrue(InternalCheckGcpEnvironment.checkBiosDataOnWindows(reader)); + reader = new BufferedReader(new StringReader("Manufacturer:Google")); + assertTrue(InternalCheckGcpEnvironment.checkBiosDataOnWindows(reader)); + reader = new BufferedReader(new StringReader("Manufacturer : Google ")); + assertTrue(InternalCheckGcpEnvironment.checkBiosDataOnWindows(reader)); + reader = new BufferedReader(new StringReader("BIOSVersion : 1.0\nManufacturer : Google\n")); + assertTrue(InternalCheckGcpEnvironment.checkBiosDataOnWindows(reader)); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AesGcmAeadCrypterTest.java b/alts/src/test/java/io/grpc/alts/internal/AesGcmAeadCrypterTest.java new file mode 100644 index 00000000000..bcf8c2810ee --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AesGcmAeadCrypterTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; + +import org.conscrypt.Conscrypt; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class AesGcmAeadCrypterTest { + @Test + public void getConscrypt_worksWhenConscryptIsAvailable() { + assume().that(Conscrypt.isAvailable()).isTrue(); + assertThat(AesGcmAeadCrypter.getConscrypt()).isNotNull(); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AesGcmHkdfAeadCrypterTest.java b/alts/src/test/java/io/grpc/alts/internal/AesGcmHkdfAeadCrypterTest.java new file mode 100644 index 00000000000..06f2ff5365f --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AesGcmHkdfAeadCrypterTest.java @@ -0,0 +1,494 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.common.io.BaseEncoding; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AesGcmHkdfAeadCrypter}. */ +@RunWith(JUnit4.class) +public final class AesGcmHkdfAeadCrypterTest { + + private static class TestVector { + final String comment; + final byte[] key; + final byte[] nonce; + final byte[] aad; + final byte[] plaintext; + final byte[] ciphertext; + + TestVector(TestVectorBuilder builder) { + comment = builder.comment; + key = builder.key; + nonce = builder.nonce; + aad = builder.aad; + plaintext = builder.plaintext; + ciphertext = builder.ciphertext; + } + + static TestVectorBuilder builder() { + return new TestVectorBuilder(); + } + } + + private static class TestVectorBuilder { + String comment; + byte[] key; + byte[] nonce; + byte[] aad; + byte[] plaintext; + byte[] ciphertext; + + TestVector build() { + if (comment == null + && key == null + && nonce == null + && aad == null + && plaintext == null + && ciphertext == null) { + throw new IllegalStateException("All fields must be set before calling build()."); + } + return new TestVector(this); + } + + TestVectorBuilder withComment(String comment) { + this.comment = comment; + return this; + } + + TestVectorBuilder withKey(String key) { + this.key = BaseEncoding.base16().lowerCase().decode(key); + return this; + } + + TestVectorBuilder withNonce(String nonce) { + this.nonce = BaseEncoding.base16().lowerCase().decode(nonce); + return this; + } + + TestVectorBuilder withAad(String aad) { + this.aad = BaseEncoding.base16().lowerCase().decode(aad); + return this; + } + + TestVectorBuilder withPlaintext(String plaintext) { + this.plaintext = BaseEncoding.base16().lowerCase().decode(plaintext); + return this; + } + + TestVectorBuilder withCiphertext(String ciphertext) { + this.ciphertext = BaseEncoding.base16().lowerCase().decode(ciphertext); + return this; + } + } + + @Test + public void testVectorEncrypt() throws GeneralSecurityException { + int i = 0; + for (TestVector testVector : testVectors) { + int bufferSize = testVector.ciphertext.length; + byte[] ciphertext = new byte[bufferSize]; + ByteBuffer ciphertextBuffer = ByteBuffer.wrap(ciphertext); + + AesGcmHkdfAeadCrypter aeadCrypter = new AesGcmHkdfAeadCrypter(testVector.key); + aeadCrypter.encrypt( + ciphertextBuffer, + ByteBuffer.wrap(testVector.plaintext), + ByteBuffer.wrap(testVector.aad), + testVector.nonce); + String msg = "Failure for test vector " + i + " " + testVector.comment; + assertWithMessage(msg) + .that(ciphertextBuffer.remaining()) + .isEqualTo(bufferSize - testVector.ciphertext.length); + byte[] exactCiphertext = Arrays.copyOf(ciphertext, testVector.ciphertext.length); + assertWithMessage(msg).that(exactCiphertext).isEqualTo(testVector.ciphertext); + i++; + } + } + + @Test + public void testVectorDecrypt() throws GeneralSecurityException { + int i = 0; + for (TestVector testVector : testVectors) { + // The plaintext buffer might require space for the tag to decrypt (e.g., for conscrypt). + int bufferSize = testVector.ciphertext.length; + byte[] plaintext = new byte[bufferSize]; + ByteBuffer plaintextBuffer = ByteBuffer.wrap(plaintext); + + AesGcmHkdfAeadCrypter aeadCrypter = new AesGcmHkdfAeadCrypter(testVector.key); + aeadCrypter.decrypt( + plaintextBuffer, + ByteBuffer.wrap(testVector.ciphertext), + ByteBuffer.wrap(testVector.aad), + testVector.nonce); + String msg = "Failure for test vector " + i + " " + testVector.comment; + assertWithMessage(msg) + .that(plaintextBuffer.remaining()) + .isEqualTo(bufferSize - testVector.plaintext.length); + byte[] exactPlaintext = Arrays.copyOf(plaintext, testVector.plaintext.length); + assertWithMessage(msg).that(exactPlaintext).isEqualTo(testVector.plaintext); + i++; + } + } + + /* + * NIST vectors from: + * http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf + * + * IEEE vectors from: + * http://www.ieee802.org/1/files/public/docs2011/bn-randall-test-vectors-0511-v1.pdf + * Key expanded by setting + * expandedKey = (key||(key ^ {0x01, .., 0x01})||key ^ {0x02,..,0x02}))[0:44]. + */ + private static final TestVector[] testVectors = + new TestVector[] { + TestVector.builder() + .withComment("Derived from NIST test vector 1") + .withKey( + "0000000000000000000000000000000001010101010101010101010101010101020202020202020202" + + "020202") + .withNonce("000000000000000000000000") + .withAad("") + .withPlaintext("") + .withCiphertext("85e873e002f6ebdc4060954eb8675508") + .build(), + TestVector.builder() + .withComment("Derived from NIST test vector 2") + .withKey( + "0000000000000000000000000000000001010101010101010101010101010101020202020202020202" + + "020202") + .withNonce("000000000000000000000000") + .withAad("") + .withPlaintext("00000000000000000000000000000000") + .withCiphertext("51e9a8cb23ca2512c8256afff8e72d681aca19a1148ac115e83df4888cc00d11") + .build(), + TestVector.builder() + .withComment("Derived from NIST test vector 3") + .withKey( + "feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f" + + "688d96") + .withNonce("cafebabefacedbaddecaf888") + .withAad("") + .withPlaintext( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532f" + + "cf0e2449a6b525b16aedf5aa0de657ba637b391aafd255") + .withCiphertext( + "1018ed5a1402a86516d6576d70b2ffccca261b94df88b58f53b64dfba435d18b2f6e3b7869f9353d4a" + + "c8cf09afb1663daa7b4017e6fc2c177c0c087c0df1162129952213cee1bc6e9c8495dd705e1f" + + "3d") + .build(), + TestVector.builder() + .withComment("Derived from NIST test vector 4") + .withKey( + "feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f" + + "688d96") + .withNonce("cafebabefacedbaddecaf888") + .withAad("feedfacedeadbeeffeedfacedeadbeefabaddad2") + .withPlaintext( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532f" + + "cf0e2449a6b525b16aedf5aa0de657ba637b39") + .withCiphertext( + "1018ed5a1402a86516d6576d70b2ffccca261b94df88b58f53b64dfba435d18b2f6e3b7869f9353d4a" + + "c8cf09afb1663daa7b4017e6fc2c177c0c087c4764565d077e9124001ddb27fc0848c5") + .build(), + TestVector.builder() + .withComment( + "Derived from adapted NIST test vector 4" + + " for KDF counter boundary (flip nonce bit 15)") + .withKey( + "feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f" + + "688d96") + .withNonce("ca7ebabefacedbaddecaf888") + .withAad("feedfacedeadbeeffeedfacedeadbeefabaddad2") + .withPlaintext( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532f" + + "cf0e2449a6b525b16aedf5aa0de657ba637b39") + .withCiphertext( + "e650d3c0fb879327f2d03287fa93cd07342b136215adbca00c3bd5099ec41832b1d18e0423ed26bb12" + + "c6cd09debb29230a94c0cee15903656f85edb6fc509b1b28216382172ecbcc31e1e9b1") + .build(), + TestVector.builder() + .withComment( + "Derived from adapted NIST test vector 4" + + " for KDF counter boundary (flip nonce bit 16)") + .withKey( + "feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f" + + "688d96") + .withNonce("cafebbbefacedbaddecaf888") + .withAad("feedfacedeadbeeffeedfacedeadbeefabaddad2") + .withPlaintext( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532f" + + "cf0e2449a6b525b16aedf5aa0de657ba637b39") + .withCiphertext( + "c0121e6c954d0767f96630c33450999791b2da2ad05c4190169ccad9ac86ff1c721e3d82f2ad22ab46" + + "3bab4a0754b7dd68ca4de7ea2531b625eda01f89312b2ab957d5c7f8568dd95fcdcd1f") + .build(), + TestVector.builder() + .withComment( + "Derived from adapted NIST test vector 4" + + " for KDF counter boundary (flip nonce bit 63)") + .withKey( + "feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f" + + "688d96") + .withNonce("cafebabefacedb2ddecaf888") + .withAad("feedfacedeadbeeffeedfacedeadbeefabaddad2") + .withPlaintext( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532f" + + "cf0e2449a6b525b16aedf5aa0de657ba637b39") + .withCiphertext( + "8af37ea5684a4d81d4fd817261fd9743099e7e6a025eaacf8e54b124fb5743149e05cb89f4a49467fe" + + "2e5e5965f29a19f99416b0016b54585d12553783ba59e9f782e82e097c336bf7989f08") + .build(), + TestVector.builder() + .withComment( + "Derived from adapted NIST test vector 4" + + " for KDF counter boundary (flip nonce bit 64)") + .withKey( + "feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f" + + "688d96") + .withNonce("cafebabefacedbaddfcaf888") + .withAad("feedfacedeadbeeffeedfacedeadbeefabaddad2") + .withPlaintext( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532f" + + "cf0e2449a6b525b16aedf5aa0de657ba637b39") + .withCiphertext( + "fbd528448d0346bfa878634864d407a35a039de9db2f1feb8e965b3ae9356ce6289441d77f8f0df294" + + "891f37ea438b223e3bf2bdc53d4c5a74fb680bb312a8dec6f7252cbcd7f5799750ad78") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.1.1 54-byte auth") + .withKey( + "ad7a2bd03eac835a6f620fdcb506b345ac7b2ad13fad825b6e630eddb407b244af7829d23cae81586d" + + "600dde") + .withNonce("12153524c0895e81b2c28465") + .withAad( + "d609b1f056637a0d46df998d88e5222ab2c2846512153524c0895e8108000f10111213141516171819" + + "1a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340001") + .withPlaintext("") + .withCiphertext("3ea0b584f3c85e93f9320ea591699efb") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.1.2 54-byte auth") + .withKey( + "e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72e1c2888d04c4e1af97" + + "a50755") + .withNonce("12153524c0895e81b2c28465") + .withAad( + "d609b1f056637a0d46df998d88e5222ab2c2846512153524c0895e8108000f10111213141516171819" + + "1a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340001") + .withPlaintext("") + .withCiphertext("294e028bf1fe6f14c4e8f7305c933eb5") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.2.1 60-byte crypt") + .withKey( + "ad7a2bd03eac835a6f620fdcb506b345ac7b2ad13fad825b6e630eddb407b244af7829d23cae81586d" + + "600dde") + .withNonce("12153524c0895e81b2c28465") + .withAad("d609b1f056637a0d46df998d88e52e00b2c2846512153524c0895e81") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435" + + "363738393a0002") + .withCiphertext( + "db3d25719c6b0a3ca6145c159d5c6ed9aff9c6e0b79f17019ea923b8665ddf52137ad611f0d1bf417a" + + "7ca85e45afe106ff9c7569d335d086ae6c03f00987ccd6") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.2.2 60-byte crypt") + .withKey( + "e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72e1c2888d04c4e1af97" + + "a50755") + .withNonce("12153524c0895e81b2c28465") + .withAad("d609b1f056637a0d46df998d88e52e00b2c2846512153524c0895e81") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435" + + "363738393a0002") + .withCiphertext( + "1641f28ec13afcc8f7903389787201051644914933e9202bb9d06aa020c2a67ef51dfe7bc00a856c55" + + "b8f8133e77f659132502bad63f5713d57d0c11e0f871ed") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.3.1 60-byte auth") + .withKey( + "071b113b0ca743fecccf3d051f737382061a103a0da642ffcdce3c041e727283051913390ea541fcce" + + "cd3f07") + .withNonce("f0761e8dcd3d000176d457ed") + .withAad( + "e20106d7cd0df0761e8dcd3d88e5400076d457ed08000f101112131415161718191a1b1c1d1e1f2021" + + "22232425262728292a2b2c2d2e2f303132333435363738393a0003") + .withPlaintext("") + .withCiphertext("58837a10562b0f1f8edbe58ca55811d3") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.3.2 60-byte auth") + .withKey( + "691d3ee909d7f54167fd1ca0b5d769081f2bde1aee655fdbab80bd5295ae6be76b1f3ceb0bd5f74365" + + "ff1ea2") + .withNonce("f0761e8dcd3d000176d457ed") + .withAad( + "e20106d7cd0df0761e8dcd3d88e5400076d457ed08000f101112131415161718191a1b1c1d1e1f2021" + + "22232425262728292a2b2c2d2e2f303132333435363738393a0003") + .withPlaintext("") + .withCiphertext("c2722ff6ca29a257718a529d1f0c6a3b") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.4.1 54-byte crypt") + .withKey( + "071b113b0ca743fecccf3d051f737382061a103a0da642ffcdce3c041e727283051913390ea541fcce" + + "cd3f07") + .withNonce("f0761e8dcd3d000176d457ed") + .withAad("e20106d7cd0df0761e8dcd3d88e54c2a76d457ed") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333400" + + "04") + .withCiphertext( + "fd96b715b93a13346af51e8acdf792cdc7b2686f8574c70e6b0cbf16291ded427ad73fec48cd298e05" + + "28a1f4c644a949fc31dc9279706ddba33f") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.4.2 54-byte crypt") + .withKey( + "691d3ee909d7f54167fd1ca0b5d769081f2bde1aee655fdbab80bd5295ae6be76b1f3ceb0bd5f74365" + + "ff1ea2") + .withNonce("f0761e8dcd3d000176d457ed") + .withAad("e20106d7cd0df0761e8dcd3d88e54c2a76d457ed") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333400" + + "04") + .withCiphertext( + "b68f6300c2e9ae833bdc070e24021a3477118e78ccf84e11a485d861476c300f175353d5cdf92008a4" + + "f878e6cc3577768085c50a0e98fda6cbb8") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.5.1 65-byte auth") + .withKey( + "013fe00b5f11be7f866d0cbbc55a7a90003ee10a5e10bf7e876c0dbac45b7b91033de2095d13bc7d84" + + "6f0eb9") + .withNonce("7cfde9f9e33724c68932d612") + .withAad( + "84c5d513d2aaf6e5bbd2727788e523008932d6127cfde9f9e33724c608000f10111213141516171819" + + "1a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" + + "0005") + .withPlaintext("") + .withCiphertext("cca20eecda6283f09bb3543dd99edb9b") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.5.2 65-byte auth") + .withKey( + "83c093b58de7ffe1c0da926ac43fb3609ac1c80fee1b624497ef942e2f79a82381c291b78fe5fde3c2" + + "d89068") + .withNonce("7cfde9f9e33724c68932d612") + .withAad( + "84c5d513d2aaf6e5bbd2727788e523008932d6127cfde9f9e33724c608000f10111213141516171819" + + "1a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" + + "0005") + .withPlaintext("") + .withCiphertext("b232cc1da5117bf15003734fa599d271") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.6.1 61-byte crypt") + .withKey( + "013fe00b5f11be7f866d0cbbc55a7a90003ee10a5e10bf7e876c0dbac45b7b91033de2095d13bc7d84" + + "6f0eb9") + .withNonce("7cfde9f9e33724c68932d612") + .withAad("84c5d513d2aaf6e5bbd2727788e52f008932d6127cfde9f9e33724c6") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435" + + "363738393a3b0006") + .withCiphertext( + "ff1910d35ad7e5657890c7c560146fd038707f204b66edbc3d161f8ace244b985921023c436e3a1c35" + + "32ecd5d09a056d70be583f0d10829d9387d07d33d872e490") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.6.2 61-byte crypt") + .withKey( + "83c093b58de7ffe1c0da926ac43fb3609ac1c80fee1b624497ef942e2f79a82381c291b78fe5fde3c2" + + "d89068") + .withNonce("7cfde9f9e33724c68932d612") + .withAad("84c5d513d2aaf6e5bbd2727788e52f008932d6127cfde9f9e33724c6") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435" + + "363738393a3b0006") + .withCiphertext( + "0db4cf956b5f97eca4eab82a6955307f9ae02a32dd7d93f83d66ad04e1cfdc5182ad12abdea5bbb619" + + "a1bd5fb9a573590fba908e9c7a46c1f7ba0905d1b55ffda4") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.7.1 79-byte crypt") + .withKey( + "88ee087fd95da9fbf6725aa9d757b0cd89ef097ed85ca8faf7735ba8d656b1cc8aec0a7ddb5fabf9f4" + + "7058ab") + .withNonce("7ae8e2ca4ec500012e58495c") + .withAad( + "68f2e77696ce7ae8e2ca4ec588e541002e58495c08000f101112131415161718191a1b1c1d1e1f2021" + + "22232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647" + + "48494a4b4c4d0007") + .withPlaintext("") + .withCiphertext("813f0e630f96fb2d030f58d83f5cdfd0") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.7.2 79-byte crypt") + .withKey( + "4c973dbc7364621674f8b5b89e5c15511fced9216490fb1c1a2caa0ffe0407e54e953fbe7166601476" + + "fab7ba") + .withNonce("7ae8e2ca4ec500012e58495c") + .withAad( + "68f2e77696ce7ae8e2ca4ec588e541002e58495c08000f101112131415161718191a1b1c1d1e1f2021" + + "22232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647" + + "48494a4b4c4d0007") + .withPlaintext("") + .withCiphertext("77e5a44c21eb07188aacbd74d1980e97") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.8.1 61-byte crypt") + .withKey( + "88ee087fd95da9fbf6725aa9d757b0cd89ef097ed85ca8faf7735ba8d656b1cc8aec0a7ddb5fabf9f4" + + "7058ab") + .withNonce("7ae8e2ca4ec500012e58495c") + .withAad("68f2e77696ce7ae8e2ca4ec588e54d002e58495c") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435" + + "363738393a3b3c3d3e3f404142434445464748490008") + .withCiphertext( + "958ec3f6d60afeda99efd888f175e5fcd4c87b9bcc5c2f5426253a8b506296c8c43309ab2adb593946" + + "2541d95e80811e04e706b1498f2c407c7fb234f8cc01a647550ee6b557b35a7e3945381821" + + "f4") + .build(), + TestVector.builder() + .withComment("Derived from IEEE 2.8.2 61-byte crypt") + .withKey( + "4c973dbc7364621674f8b5b89e5c15511fced9216490fb1c1a2caa0ffe0407e54e953fbe7166601476" + + "fab7ba") + .withNonce("7ae8e2ca4ec500012e58495c") + .withAad("68f2e77696ce7ae8e2ca4ec588e54d002e58495c") + .withPlaintext( + "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435" + + "363738393a3b3c3d3e3f404142434445464748490008") + .withCiphertext( + "b44d072011cd36d272a9b7a98db9aa90cbc5c67b93ddce67c854503214e2e896ec7e9db649ed4bcf6f" + + "850aac0223d0cf92c83db80795c3a17ecc1248bb00591712b1ae71e268164196252162810b" + + "00") + .build() + }; +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsChannelCrypterTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsChannelCrypterTest.java new file mode 100644 index 00000000000..0b40a8b31df --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsChannelCrypterTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.alts.internal.AltsChannelCrypter.incrementCounter; +import static org.junit.Assert.fail; + +import com.google.common.testing.GcFinalization; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetector.Level; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsChannelCrypter}. */ +@RunWith(JUnit4.class) +public final class AltsChannelCrypterTest extends ChannelCrypterNettyTestBase { + + @Before + public void setUp() throws GeneralSecurityException { + ResourceLeakDetector.setLevel(Level.PARANOID); + client = new AltsChannelCrypter(new byte[AltsChannelCrypter.getKeyLength()], true); + server = new AltsChannelCrypter(new byte[AltsChannelCrypter.getKeyLength()], false); + } + + @After + public void tearDown() throws GeneralSecurityException { + for (ReferenceCounted reference : references) { + reference.release(); + } + references.clear(); + client.destroy(); + server.destroy(); + // Increase our chances to detect ByteBuf leaks. + GcFinalization.awaitFullGc(); + } + + @Test + public void encryptDecryptKdfCounterIncr() throws GeneralSecurityException { + AltsChannelCrypter client = + new AltsChannelCrypter(new byte[AltsChannelCrypter.getKeyLength()], true); + AltsChannelCrypter server = + new AltsChannelCrypter(new byte[AltsChannelCrypter.getKeyLength()], false); + + String message = "Hello world"; + FrameEncrypt frameEncrypt1 = createFrameEncrypt(message); + + client.encrypt(frameEncrypt1.out, frameEncrypt1.plain); + FrameDecrypt frameDecrypt1 = frameDecryptOfEncrypt(frameEncrypt1); + + server.decrypt(frameDecrypt1.out, frameDecrypt1.tag, frameDecrypt1.ciphertext); + assertThat(frameEncrypt1.plain.get(0).slice(0, frameDecrypt1.out.readableBytes())) + .isEqualTo(frameDecrypt1.out); + + // Increase counters to get a new KDF counter value (first two bytes are skipped). + client.incrementOutCounterForTesting(1 << 17); + server.incrementInCounterForTesting(1 << 17); + + FrameEncrypt frameEncrypt2 = createFrameEncrypt(message); + + client.encrypt(frameEncrypt2.out, frameEncrypt2.plain); + FrameDecrypt frameDecrypt2 = frameDecryptOfEncrypt(frameEncrypt2); + + server.decrypt(frameDecrypt2.out, frameDecrypt2.tag, frameDecrypt2.ciphertext); + assertThat(frameEncrypt2.plain.get(0).slice(0, frameDecrypt2.out.readableBytes())) + .isEqualTo(frameDecrypt2.out); + } + + @Test + public void overflowsClient() throws GeneralSecurityException { + byte[] maxFirst = + new byte[] { + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 + }; + + byte[] maxFirstPred = Arrays.copyOf(maxFirst, maxFirst.length); + maxFirstPred[0]--; + + byte[] oldCounter = new byte[AltsChannelCrypter.getCounterLength()]; + byte[] counter = Arrays.copyOf(maxFirstPred, maxFirstPred.length); + + incrementCounter(counter, oldCounter); + + assertThat(oldCounter).isEqualTo(maxFirstPred); + assertThat(counter).isEqualTo(maxFirst); + + try { + incrementCounter(counter, oldCounter); + fail("Exception expected"); + } catch (GeneralSecurityException ex) { + assertThat(ex).hasMessageThat().contains("Counter has overflowed"); + } + + assertThat(oldCounter).isEqualTo(maxFirst); + assertThat(counter).isEqualTo(maxFirst); + } + + @Test + public void overflowsServer() throws GeneralSecurityException { + byte[] maxSecond = + new byte[] { + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80 + }; + + byte[] maxSecondPred = Arrays.copyOf(maxSecond, maxSecond.length); + maxSecondPred[0]--; + + byte[] oldCounter = new byte[AltsChannelCrypter.getCounterLength()]; + byte[] counter = Arrays.copyOf(maxSecondPred, maxSecondPred.length); + + incrementCounter(counter, oldCounter); + + assertThat(oldCounter).isEqualTo(maxSecondPred); + assertThat(counter).isEqualTo(maxSecond); + + try { + incrementCounter(counter, oldCounter); + fail("Exception expected"); + } catch (GeneralSecurityException ex) { + assertThat(ex).hasMessageThat().contains("Counter has overflowed"); + } + + assertThat(oldCounter).isEqualTo(maxSecond); + assertThat(counter).isEqualTo(maxSecond); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsClientOptionsTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsClientOptionsTest.java new file mode 100644 index 00000000000..45fac0aa301 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsClientOptionsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsClientOptions}. */ +@RunWith(JUnit4.class) +public final class AltsClientOptionsTest { + + @Test + public void setAndGet() throws Exception { + String targetName = "foo"; + String serviceAccount1 = "bar1"; + String serviceAccount2 = "bar2"; + RpcProtocolVersions rpcVersions = + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion( + RpcProtocolVersions.Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion( + RpcProtocolVersions.Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(); + ImmutableList serviceAccounts = ImmutableList.of(serviceAccount1, serviceAccount2); + + AltsClientOptions options = + new AltsClientOptions.Builder() + .setTargetName(targetName) + .setTargetServiceAccounts(serviceAccounts) + .setRpcProtocolVersions(rpcVersions) + .build(); + + assertThat(options.getTargetName()).isEqualTo(targetName); + assertThat(options.getTargetServiceAccounts()) + .containsAtLeast(serviceAccount1, serviceAccount2); + assertThat(options.getRpcProtocolVersions()).isEqualTo(rpcVersions); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsFramingTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsFramingTest.java new file mode 100644 index 00000000000..a4703e052ee --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsFramingTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.GeneralSecurityException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsFraming}. */ +@RunWith(JUnit4.class) +public class AltsFramingTest { + + @Test + public void parserFrameLengthNegativeFails() throws GeneralSecurityException { + AltsFraming.Parser parser = new AltsFraming.Parser(); + // frame length + one remaining byte (required) + ByteBuffer buffer = ByteBuffer.allocate(AltsFraming.getFrameLengthHeaderSize() + 1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(-1); // write invalid length + buffer.put((byte) 0); // write some byte + ((Buffer) buffer).flip(); + + try { + parser.readBytes(buffer); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid frame length"); + } + } + + @Test + public void parserFrameLengthSmallerMessageTypeFails() throws GeneralSecurityException { + AltsFraming.Parser parser = new AltsFraming.Parser(); + // frame length + one remaining byte (required) + ByteBuffer buffer = ByteBuffer.allocate(AltsFraming.getFrameLengthHeaderSize() + 1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(AltsFraming.getFrameMessageTypeHeaderSize() - 1); // write invalid length + buffer.put((byte) 0); // write some byte + ((Buffer) buffer).flip(); + + try { + parser.readBytes(buffer); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid frame length"); + } + } + + @Test + public void parserFrameLengthTooLargeFails() throws GeneralSecurityException { + AltsFraming.Parser parser = new AltsFraming.Parser(); + // frame length + one remaining byte (required) + ByteBuffer buffer = ByteBuffer.allocate(AltsFraming.getFrameLengthHeaderSize() + 1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(AltsFraming.getMaxDataLength() + 1); // write invalid length + buffer.put((byte) 0); // write some byte + ((Buffer) buffer).flip(); + + try { + parser.readBytes(buffer); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid frame length"); + } + } + + @Test + public void parserFrameLengthMaxOk() throws GeneralSecurityException { + AltsFraming.Parser parser = new AltsFraming.Parser(); + // length of type header + data + int dataLength = AltsFraming.getMaxDataLength(); + // complete frame + 1 byte + ByteBuffer buffer = + ByteBuffer.allocate(AltsFraming.getFrameLengthHeaderSize() + dataLength + 1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(dataLength); // write invalid length + buffer.putInt(6); // default message type + buffer.put(new byte[dataLength - AltsFraming.getFrameMessageTypeHeaderSize()]); // write data + buffer.put((byte) 0); + ((Buffer) buffer).flip(); + + parser.readBytes(buffer); + + assertThat(parser.isComplete()).isTrue(); + assertThat(buffer.remaining()).isEqualTo(1); + } + + @Test + public void parserFrameLengthZeroOk() throws GeneralSecurityException { + AltsFraming.Parser parser = new AltsFraming.Parser(); + int dataLength = AltsFraming.getFrameMessageTypeHeaderSize(); + // complete frame + 1 byte + ByteBuffer buffer = + ByteBuffer.allocate(AltsFraming.getFrameLengthHeaderSize() + dataLength + 1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(dataLength); // write invalid length + buffer.putInt(6); // default message type + buffer.put((byte) 0); + ((Buffer) buffer).flip(); + + parser.readBytes(buffer); + + assertThat(parser.isComplete()).isTrue(); + assertThat(buffer.remaining()).isEqualTo(1); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerClientTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerClientTest.java new file mode 100644 index 00000000000..5a41fc0fc4f --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerClientTest.java @@ -0,0 +1,280 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.ByteString; +import io.grpc.internal.TestUtils.NoopChannelLogger; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +/** Unit tests for {@link AltsHandshakerClient}. */ +@RunWith(JUnit4.class) +public class AltsHandshakerClientTest { + private static final int IN_BYTES_SIZE = 100; + private static final int BYTES_CONSUMED = 30; + private static final int PREFIX_POSITION = 20; + private static final String TEST_TARGET_NAME = "target name"; + private static final String TEST_TARGET_SERVICE_ACCOUNT = "peer service account"; + + private AltsHandshakerStub mockStub; + private AltsHandshakerClient handshaker; + private AltsClientOptions clientOptions; + + @Before + public void setUp() { + mockStub = mock(AltsHandshakerStub.class); + clientOptions = + new AltsClientOptions.Builder() + .setTargetName(TEST_TARGET_NAME) + .setTargetServiceAccounts(ImmutableList.of(TEST_TARGET_SERVICE_ACCOUNT)) + .build(); + NoopChannelLogger channelLogger = new NoopChannelLogger(); + handshaker = new AltsHandshakerClient(mockStub, clientOptions, channelLogger); + } + + @Test + public void startClientHandshakeFailure() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getErrorResponse()); + + try { + handshaker.startClientHandshake(); + fail("Exception expected"); + } catch (GeneralSecurityException ex) { + assertThat(ex).hasMessageThat().contains(MockAltsHandshakerResp.getTestErrorDetails()); + } + } + + @Test + public void startClientHandshakeSuccess() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getOkResponse(0)); + + ByteBuffer outFrame = handshaker.startClientHandshake(); + + assertEquals(ByteString.copyFrom(outFrame), MockAltsHandshakerResp.getOutFrame()); + assertFalse(handshaker.isFinished()); + assertNull(handshaker.getResult()); + assertNull(handshaker.getKey()); + } + + @Test + public void startClientHandshakeWithOptions() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getOkResponse(0)); + + ByteBuffer outFrame = handshaker.startClientHandshake(); + assertEquals(ByteString.copyFrom(outFrame), MockAltsHandshakerResp.getOutFrame()); + + HandshakerReq req = + HandshakerReq.newBuilder() + .setClientStart( + StartClientHandshakeReq.newBuilder() + .setHandshakeSecurityProtocol(HandshakeProtocol.ALTS) + .addApplicationProtocols(AltsHandshakerClient.getApplicationProtocol()) + .addRecordProtocols(AltsHandshakerClient.getRecordProtocol()) + .setTargetName(TEST_TARGET_NAME) + .addTargetIdentities( + Identity.newBuilder().setServiceAccount(TEST_TARGET_SERVICE_ACCOUNT)) + .setMaxFrameSize(AltsTsiFrameProtector.getMaxFrameSize()) + .build()) + .build(); + verify(mockStub).send(req); + } + + @Test + public void startServerHandshakeFailure() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getErrorResponse()); + + try { + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + handshaker.startServerHandshake(inBytes); + fail("Exception expected"); + } catch (GeneralSecurityException ex) { + assertThat(ex).hasMessageThat().contains(MockAltsHandshakerResp.getTestErrorDetails()); + } + } + + @Test + public void startServerHandshakeSuccess() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getOkResponse(BYTES_CONSUMED)); + + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + ByteBuffer outFrame = handshaker.startServerHandshake(inBytes); + + HandshakerReq req = + HandshakerReq.newBuilder() + .setServerStart( + StartServerHandshakeReq.newBuilder() + .addApplicationProtocols(AltsHandshakerClient.getApplicationProtocol()) + .putHandshakeParameters( + HandshakeProtocol.ALTS.getNumber(), + ServerHandshakeParameters.newBuilder() + .addRecordProtocols(AltsHandshakerClient.getRecordProtocol()) + .build()) + .setInBytes(ByteString.copyFrom(ByteBuffer.allocate(IN_BYTES_SIZE))) + .setMaxFrameSize(AltsTsiFrameProtector.getMaxFrameSize()) + .build()) + .build(); + verify(mockStub).send(req); + + assertEquals(ByteString.copyFrom(outFrame), MockAltsHandshakerResp.getOutFrame()); + assertFalse(handshaker.isFinished()); + assertNull(handshaker.getResult()); + assertNull(handshaker.getKey()); + assertEquals(IN_BYTES_SIZE - BYTES_CONSUMED, inBytes.remaining()); + } + + @Test + public void startServerHandshakeEmptyOutFrame() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getEmptyOutFrameResponse(BYTES_CONSUMED)); + + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + ByteBuffer outFrame = handshaker.startServerHandshake(inBytes); + + assertEquals(0, outFrame.remaining()); + assertFalse(handshaker.isFinished()); + assertNull(handshaker.getResult()); + assertNull(handshaker.getKey()); + assertEquals(IN_BYTES_SIZE - BYTES_CONSUMED, inBytes.remaining()); + } + + @Test + public void startServerHandshakeWithPrefixBuffer() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getOkResponse(BYTES_CONSUMED)); + + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + ((Buffer) inBytes).position(PREFIX_POSITION); + ByteBuffer outFrame = handshaker.startServerHandshake(inBytes); + + assertEquals(ByteString.copyFrom(outFrame), MockAltsHandshakerResp.getOutFrame()); + assertFalse(handshaker.isFinished()); + assertNull(handshaker.getResult()); + assertNull(handshaker.getKey()); + assertEquals(PREFIX_POSITION + BYTES_CONSUMED, inBytes.position()); + assertEquals(IN_BYTES_SIZE - BYTES_CONSUMED - PREFIX_POSITION, inBytes.remaining()); + } + + @Test + public void nextFailure() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getErrorResponse()); + + try { + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + handshaker.next(inBytes); + fail("Exception expected"); + } catch (GeneralSecurityException ex) { + assertThat(ex).hasMessageThat().contains(MockAltsHandshakerResp.getTestErrorDetails()); + } + } + + @Test + public void nextSuccess() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getOkResponse(BYTES_CONSUMED)); + + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + ByteBuffer outFrame = handshaker.next(inBytes); + + assertEquals(ByteString.copyFrom(outFrame), MockAltsHandshakerResp.getOutFrame()); + assertFalse(handshaker.isFinished()); + assertNull(handshaker.getResult()); + assertNull(handshaker.getKey()); + assertEquals(IN_BYTES_SIZE - BYTES_CONSUMED, inBytes.remaining()); + } + + @Test + public void nextEmptyOutFrame() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getEmptyOutFrameResponse(BYTES_CONSUMED)); + + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + ByteBuffer outFrame = handshaker.next(inBytes); + + assertEquals(0, outFrame.remaining()); + assertFalse(handshaker.isFinished()); + assertNull(handshaker.getResult()); + assertNull(handshaker.getKey()); + assertEquals(IN_BYTES_SIZE - BYTES_CONSUMED, inBytes.remaining()); + } + + @Test + public void nextFinished() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getFinishedResponse(BYTES_CONSUMED)); + + ByteBuffer inBytes = ByteBuffer.allocate(IN_BYTES_SIZE); + ByteBuffer outFrame = handshaker.next(inBytes); + + assertEquals(ByteString.copyFrom(outFrame), MockAltsHandshakerResp.getOutFrame()); + assertTrue(handshaker.isFinished()); + assertArrayEquals(handshaker.getKey(), MockAltsHandshakerResp.getTestKeyData()); + assertEquals(IN_BYTES_SIZE - BYTES_CONSUMED, inBytes.remaining()); + } + + @Test + public void setRpcVersions() throws Exception { + when(mockStub.send(ArgumentMatchers.any())) + .thenReturn(MockAltsHandshakerResp.getOkResponse(0)); + + RpcProtocolVersions rpcVersions = + RpcProtocolVersions.newBuilder() + .setMinRpcVersion( + RpcProtocolVersions.Version.newBuilder().setMajor(3).setMinor(4).build()) + .setMaxRpcVersion( + RpcProtocolVersions.Version.newBuilder().setMajor(5).setMinor(6).build()) + .build(); + clientOptions = + new AltsClientOptions.Builder() + .setTargetName(TEST_TARGET_NAME) + .setTargetServiceAccounts(ImmutableList.of(TEST_TARGET_SERVICE_ACCOUNT)) + .setRpcProtocolVersions(rpcVersions) + .build(); + NoopChannelLogger channelLogger = new NoopChannelLogger(); + handshaker = new AltsHandshakerClient(mockStub, clientOptions, channelLogger); + + handshaker.startClientHandshake(); + + ArgumentCaptor reqCaptor = ArgumentCaptor.forClass(HandshakerReq.class); + verify(mockStub).send(reqCaptor.capture()); + assertEquals(rpcVersions, reqCaptor.getValue().getClientStart().getRpcVersions()); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerOptionsTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerOptionsTest.java new file mode 100644 index 00000000000..ab065f2f2ef --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerOptionsTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsHandshakerOptions}. */ +@RunWith(JUnit4.class) +public final class AltsHandshakerOptionsTest { + + @Test + public void setAndGet() throws Exception { + RpcProtocolVersions rpcVersions = + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion( + RpcProtocolVersions.Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion( + RpcProtocolVersions.Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(); + + AltsHandshakerOptions options = new AltsHandshakerOptions(rpcVersions); + assertThat(options.getRpcProtocolVersions()).isEqualTo(rpcVersions); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerStubTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerStubTest.java new file mode 100644 index 00000000000..7a6018b5064 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsHandshakerStubTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.protobuf.ByteString; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsHandshakerStub}. */ +@RunWith(JUnit4.class) +public class AltsHandshakerStubTest { + /** Mock status of handshaker service. */ + private enum Status { + OK, + ERROR, + COMPLETE + } + + private AltsHandshakerStub stub; + private MockWriter writer; + + @Before + public void setUp() { + writer = new MockWriter(); + stub = new AltsHandshakerStub(writer); + writer.setReader(stub.getReaderForTest()); + } + + /** Send a message as in_bytes and expect same message as out_frames echo back. */ + private void sendSuccessfulMessage() throws Exception { + String message = "hello world"; + HandshakerReq.Builder req = + HandshakerReq.newBuilder() + .setNext( + NextHandshakeMessageReq.newBuilder() + .setInBytes(ByteString.copyFromUtf8(message)) + .build()); + HandshakerResp resp = stub.send(req.build()); + assertEquals(resp.getOutFrames().toStringUtf8(), message); + } + + /** Send a message and expect an IOException on error. */ + private void sendAndExpectError() throws InterruptedException { + try { + stub.send(HandshakerReq.newBuilder().build()); + fail("Exception expected"); + } catch (IOException ex) { + assertThat(ex).hasMessageThat().contains("Received a terminating error"); + assertThat(ex.getCause()).hasMessageThat().contains("Root cause message"); + } + } + + /** Send a message and expect an IOException on closing. */ + private void sendAndExpectComplete() throws InterruptedException { + try { + stub.send(HandshakerReq.newBuilder().build()); + fail("Exception expected"); + } catch (IOException ex) { + assertThat(ex).hasMessageThat().contains("Response stream closed"); + } + } + + /** Send a message and expect an IOException on unexpected message. */ + private void sendAndExpectUnexpectedMessage() throws InterruptedException { + try { + stub.send(HandshakerReq.newBuilder().build()); + fail("Exception expected"); + } catch (IOException ex) { + assertThat(ex).hasMessageThat().contains("Received an unexpected response"); + } + } + + @Test + public void sendSuccessfulMessageTest() throws Exception { + writer.setServiceStatus(Status.OK); + sendSuccessfulMessage(); + stub.close(); + } + + @Test + public void getServiceErrorTest() throws InterruptedException { + writer.setServiceStatus(Status.ERROR); + sendAndExpectError(); + stub.close(); + } + + @Test + public void getServiceCompleteTest() throws Exception { + writer.setServiceStatus(Status.COMPLETE); + sendAndExpectComplete(); + stub.close(); + } + + @Test + public void getUnexpectedMessageTest() throws Exception { + writer.setServiceStatus(Status.OK); + writer.sendUnexpectedResponse(); + sendAndExpectUnexpectedMessage(); + stub.close(); + } + + @Test + public void closeEarlyTest() throws InterruptedException { + stub.close(); + sendAndExpectComplete(); + } + + private static class MockWriter implements StreamObserver { + private StreamObserver reader; + private Status status = Status.OK; + + private void setReader(StreamObserver reader) { + this.reader = reader; + } + + private void setServiceStatus(Status status) { + this.status = status; + } + + /** Send a handshaker response to reader. */ + private void sendUnexpectedResponse() { + reader.onNext(HandshakerResp.newBuilder().build()); + } + + /** Mock writer onNext. Will respond based on the server status. */ + @Override + public void onNext(final HandshakerReq req) { + switch (status) { + case OK: + HandshakerResp.Builder resp = HandshakerResp.newBuilder(); + reader.onNext(resp.setOutFrames(req.getNext().getInBytes()).build()); + break; + case ERROR: + reader.onError(new RuntimeException("Root cause message")); + break; + case COMPLETE: + reader.onCompleted(); + break; + default: + return; + } + } + + @Override + public void onError(Throwable t) {} + + /** Mock writer onComplete. */ + @Override + public void onCompleted() { + reader.onCompleted(); + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsInternalContextTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsInternalContextTest.java new file mode 100644 index 00000000000..87898a3300b --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsInternalContextTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsInternalContext}. */ +@RunWith(JUnit4.class) +public final class AltsInternalContextTest { + private static final int TEST_MAX_RPC_VERSION_MAJOR = 3; + private static final int TEST_MAX_RPC_VERSION_MINOR = 5; + private static final int TEST_MIN_RPC_VERSION_MAJOR = 2; + private static final int TEST_MIN_RPC_VERSION_MINOR = 1; + private static final SecurityLevel TEST_SECURITY_LEVEL = SecurityLevel.INTEGRITY_AND_PRIVACY; + private static final String TEST_APPLICATION_PROTOCOL = "grpc"; + private static final String TEST_LOCAL_SERVICE_ACCOUNT = "local@gserviceaccount.com"; + private static final String TEST_PEER_SERVICE_ACCOUNT = "peer@gserviceaccount.com"; + private static final String TEST_RECORD_PROTOCOL = "ALTSRP_GCM_AES128"; + private static final String TEST_PEER_ATTRIBUTES_KEY = "peer"; + private static final String TEST_PEER_ATTRIBUTES_VALUE = "attributes"; + + private Map testPeerAttributes; + private HandshakerResult handshakerResult; + private RpcProtocolVersions rpcVersions; + + @Before + public void setUp() { + testPeerAttributes = new HashMap(); + testPeerAttributes.put(TEST_PEER_ATTRIBUTES_KEY, TEST_PEER_ATTRIBUTES_VALUE); + rpcVersions = + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion( + RpcProtocolVersions.Version.newBuilder() + .setMajor(TEST_MAX_RPC_VERSION_MAJOR) + .setMinor(TEST_MAX_RPC_VERSION_MINOR) + .build()) + .setMinRpcVersion( + RpcProtocolVersions.Version.newBuilder() + .setMajor(TEST_MIN_RPC_VERSION_MAJOR) + .setMinor(TEST_MIN_RPC_VERSION_MINOR) + .build()) + .build(); + Identity.Builder peerIdentity = Identity.newBuilder() + .setServiceAccount(TEST_PEER_SERVICE_ACCOUNT); + peerIdentity.putAllAttributes(testPeerAttributes); + handshakerResult = + HandshakerResult.newBuilder() + .setApplicationProtocol(TEST_APPLICATION_PROTOCOL) + .setRecordProtocol(TEST_RECORD_PROTOCOL) + .setPeerIdentity(peerIdentity) + .setLocalIdentity(Identity.newBuilder().setServiceAccount(TEST_LOCAL_SERVICE_ACCOUNT)) + .setPeerRpcVersions(rpcVersions) + .build(); + } + + @Test + public void testAltsInternalContext() { + AltsInternalContext context = new AltsInternalContext(handshakerResult); + assertEquals(TEST_APPLICATION_PROTOCOL, context.getApplicationProtocol()); + assertEquals(TEST_RECORD_PROTOCOL, context.getRecordProtocol()); + assertEquals(TEST_SECURITY_LEVEL, context.getSecurityLevel()); + assertEquals(TEST_PEER_SERVICE_ACCOUNT, context.getPeerServiceAccount()); + assertEquals(TEST_LOCAL_SERVICE_ACCOUNT, context.getLocalServiceAccount()); + assertEquals(rpcVersions, context.getPeerRpcVersions()); + assertEquals(testPeerAttributes, context.getPeerAttributes()); + assertEquals(TEST_PEER_ATTRIBUTES_VALUE, context.getPeerAttributes() + .get(TEST_PEER_ATTRIBUTES_KEY)); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsProtocolNegotiatorTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsProtocolNegotiatorTest.java new file mode 100644 index 00000000000..24392af75fd --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsProtocolNegotiatorTest.java @@ -0,0 +1,559 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import io.grpc.Attributes; +import io.grpc.Channel; +import io.grpc.ChannelLogger; +import io.grpc.Grpc; +import io.grpc.InternalChannelz; +import io.grpc.ManagedChannel; +import io.grpc.SecurityLevel; +import io.grpc.alts.internal.AltsProtocolNegotiator.LazyChannel; +import io.grpc.alts.internal.AltsProtocolNegotiator.ServerAltsProtocolNegotiator; +import io.grpc.alts.internal.TsiFrameProtector.Consumer; +import io.grpc.alts.internal.TsiPeer.Property; +import io.grpc.internal.FixedObjectPool; +import io.grpc.internal.GrpcAttributes; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.TestUtils.NoopChannelLogger; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.InternalProtocolNegotiationEvent; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; +import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; +import io.netty.handler.codec.http2.DefaultHttp2FrameReader; +import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2ConnectionDecoder; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2FrameReader; +import io.netty.handler.codec.http2.Http2FrameWriter; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AltsProtocolNegotiator}. */ +@RunWith(JUnit4.class) +@SuppressWarnings("FutureReturnValueIgnored") +public class AltsProtocolNegotiatorTest { + + private final CapturingGrpcHttp2ConnectionHandler grpcHandler = capturingGrpcHandler(); + + private final List references = new ArrayList<>(); + private final LinkedBlockingQueue protectors = new LinkedBlockingQueue<>(); + + private EmbeddedChannel channel; + private Throwable caughtException; + + private TsiPeer mockedTsiPeer = new TsiPeer(Collections.>emptyList()); + private AltsInternalContext mockedAltsContext = + new AltsInternalContext( + HandshakerResult.newBuilder() + .setPeerRpcVersions(RpcProtocolVersionsUtil.getRpcProtocolVersions()) + .build()); + private final TsiHandshaker mockHandshaker = + new DelegatingTsiHandshaker(FakeTsiHandshaker.newFakeHandshakerServer()) { + @Override + public TsiPeer extractPeer() { + return mockedTsiPeer; + } + + @Override + public Object extractPeerObject() { + return mockedAltsContext; + } + }; + private final NettyTsiHandshaker serverHandshaker = new NettyTsiHandshaker(mockHandshaker); + + @Before + public void setup() throws Exception { + ChannelHandler uncaughtExceptionHandler = + new ChannelDuplexHandler() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + caughtException = cause; + super.exceptionCaught(ctx, cause); + ctx.close(); + } + }; + + TsiHandshakerFactory handshakerFactory = + new DelegatingTsiHandshakerFactory(FakeTsiHandshaker.clientHandshakerFactory()) { + @Override + public TsiHandshaker newHandshaker(String authority, ChannelLogger logger) { + return new DelegatingTsiHandshaker(super.newHandshaker(authority, logger)) { + @Override + public TsiPeer extractPeer() throws GeneralSecurityException { + return mockedTsiPeer; + } + + @Override + public Object extractPeerObject() throws GeneralSecurityException { + return mockedAltsContext; + } + }; + } + }; + ManagedChannel fakeChannel = NettyChannelBuilder.forTarget("localhost:8080").build(); + ObjectPool fakeChannelPool = new FixedObjectPool(fakeChannel); + LazyChannel lazyFakeChannel = new LazyChannel(fakeChannelPool); + ChannelHandler altsServerHandler = new ServerAltsProtocolNegotiator( + handshakerFactory, lazyFakeChannel) + .newHandler(grpcHandler); + // On real server, WBAEH fires default ProtocolNegotiationEvent. KickNH provides this behavior. + ChannelHandler handler = new KickNegotiationHandler(altsServerHandler); + channel = new EmbeddedChannel(uncaughtExceptionHandler, handler); + } + + @After + public void teardown() throws Exception { + if (channel != null) { + @SuppressWarnings("unused") // go/futurereturn-lsc + Future possiblyIgnoredError = channel.close(); + } + + for (ReferenceCounted reference : references) { + ReferenceCountUtil.safeRelease(reference); + } + } + + @Test + public void handshakeShouldBeSuccessful() throws Exception { + doHandshake(); + } + + @Test + @SuppressWarnings("unchecked") // List cast + public void protectShouldRoundtrip() throws Exception { + doHandshake(); + + // Write the message 1 character at a time. The message should be buffered + // and not interfere with the handshake. + final AtomicInteger writeCount = new AtomicInteger(); + String message = "hello"; + for (int ix = 0; ix < message.length(); ++ix) { + ByteBuf in = Unpooled.copiedBuffer(message, ix, 1, UTF_8); + @SuppressWarnings("unused") // go/futurereturn-lsc + Future possiblyIgnoredError = + channel + .write(in) + .addListener( + new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + writeCount.incrementAndGet(); + } + } + }); + } + channel.flush(); + + // Capture the protected data written to the wire. + assertEquals(1, channel.outboundMessages().size()); + ByteBuf protectedData = channel.readOutbound(); + assertEquals(message.length(), writeCount.get()); + + // Read the protected message at the server and verify it matches the original message. + TsiFrameProtector serverProtector = serverHandshaker.createFrameProtector(channel.alloc()); + List unprotected = new ArrayList<>(); + serverProtector.unprotect(protectedData, (List) (List) unprotected, channel.alloc()); + // We try our best to remove the HTTP2 handler as soon as possible, but just by constructing it + // a settings frame is written (and an HTTP2 preface). This is hard coded into Netty, so we + // have to remove it here. See {@code Http2ConnectionHandler.PrefaceDecode.sendPreface}. + int settingsFrameLength = 9; + + CompositeByteBuf unprotectedAll = + new CompositeByteBuf(channel.alloc(), false, unprotected.size() + 1, unprotected); + ByteBuf unprotectedData = unprotectedAll.slice(settingsFrameLength, message.length()); + assertEquals(message, unprotectedData.toString(UTF_8)); + + // Protect the same message at the server. + final AtomicReference newlyProtectedData = new AtomicReference<>(); + serverProtector.protectFlush( + Collections.singletonList(unprotectedData), + new Consumer() { + @Override + public void accept(ByteBuf buf) { + newlyProtectedData.set(buf); + } + }, + channel.alloc()); + + // Read the protected message at the client and verify that it matches the original message. + channel.writeInbound(newlyProtectedData.get()); + assertEquals(1, channel.inboundMessages().size()); + assertEquals(message, channel.readInbound().toString(UTF_8)); + } + + @Test + public void unprotectLargeIncomingFrame() throws Exception { + + // We use a server frameprotector with twice the standard frame size. + int serverFrameSize = 4096 * 2; + // This should fit into one frame. + byte[] unprotectedBytes = new byte[serverFrameSize - 500]; + Arrays.fill(unprotectedBytes, (byte) 7); + ByteBuf unprotectedData = Unpooled.wrappedBuffer(unprotectedBytes); + unprotectedData.writerIndex(unprotectedBytes.length); + + // Perform handshake. + doHandshake(); + + // Protect the message on the server. + TsiFrameProtector serverProtector = + serverHandshaker.createFrameProtector(serverFrameSize, channel.alloc()); + serverProtector.protectFlush( + Collections.singletonList(unprotectedData), + new Consumer() { + @Override + public void accept(ByteBuf buf) { + channel.writeInbound(buf); + } + }, + channel.alloc()); + channel.flushInbound(); + + // Read the protected message at the client and verify that it matches the original message. + assertEquals(1, channel.inboundMessages().size()); + + ByteBuf receivedData1 = channel.readInbound(); + int receivedLen1 = receivedData1.readableBytes(); + byte[] receivedBytes = new byte[receivedLen1]; + receivedData1.readBytes(receivedBytes, 0, receivedLen1); + + assertThat(unprotectedBytes.length).isEqualTo(receivedBytes.length); + assertThat(unprotectedBytes).isEqualTo(receivedBytes); + } + + @Test + public void flushShouldFailAllPromises() throws Exception { + doHandshake(); + + channel + .pipeline() + .addFirst( + new ChannelDuplexHandler() { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + throw new Exception("Fake exception"); + } + }); + + // Write the message 1 character at a time. + String message = "hello"; + final AtomicInteger failures = new AtomicInteger(); + for (int ix = 0; ix < message.length(); ++ix) { + ByteBuf in = Unpooled.copiedBuffer(message, ix, 1, UTF_8); + @SuppressWarnings("unused") // go/futurereturn-lsc + Future possiblyIgnoredError = + channel + .write(in) + .addListener( + new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + failures.incrementAndGet(); + } + } + }); + } + channel.flush(); + + // Verify that the promises fail. + assertEquals(message.length(), failures.get()); + } + + @Test + public void doNotFlushEmptyBuffer() throws Exception { + doHandshake(); + assertEquals(1, protectors.size()); + InterceptingProtector protector = protectors.poll(); + + String message = "hello"; + ByteBuf in = Unpooled.copiedBuffer(message, UTF_8); + + assertEquals(0, protector.flushes.get()); + Future done = channel.write(in); + channel.flush(); + done.get(5, TimeUnit.SECONDS); + assertEquals(1, protector.flushes.get()); + + done = channel.write(Unpooled.EMPTY_BUFFER); + channel.flush(); + done.get(5, TimeUnit.SECONDS); + assertEquals(1, protector.flushes.get()); + } + + @Test + public void peerPropagated() throws Exception { + doHandshake(); + + assertThat(grpcHandler.attrs.get(AltsProtocolNegotiator.TSI_PEER_KEY)).isEqualTo(mockedTsiPeer); + assertThat(grpcHandler.attrs.get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY)) + .isEqualTo(mockedAltsContext); + assertThat(grpcHandler.attrs.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR).toString()) + .isEqualTo("embedded"); + assertThat(grpcHandler.attrs.get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR).toString()) + .isEqualTo("embedded"); + assertThat(grpcHandler.attrs.get(GrpcAttributes.ATTR_SECURITY_LEVEL)) + .isEqualTo(SecurityLevel.PRIVACY_AND_INTEGRITY); + } + + @Test + public void getAltsMaxConcurrentHandshakes_success() throws Exception { + assertThat(AltsProtocolNegotiator.getAltsMaxConcurrentHandshakes("10")).isEqualTo(10); + } + + @Test + public void getAltsMaxConcurrentHandshakes_envVariableNotSet() throws Exception { + assertThat(AltsProtocolNegotiator.getAltsMaxConcurrentHandshakes(null)) + .isEqualTo(AltsProtocolNegotiator.DEFAULT_ALTS_MAX_CONCURRENT_HANDSHAKES); + } + + @Test + public void getAltsMaxConcurrentHandshakes_envVariableNotANumber() throws Exception { + assertThat(AltsProtocolNegotiator.getAltsMaxConcurrentHandshakes("not-a-number")) + .isEqualTo(AltsProtocolNegotiator.DEFAULT_ALTS_MAX_CONCURRENT_HANDSHAKES); + } + + @Test + public void getAltsMaxConcurrentHandshakes_envVariableNegative() throws Exception { + assertThat(AltsProtocolNegotiator.getAltsMaxConcurrentHandshakes("-10")) + .isEqualTo(AltsProtocolNegotiator.DEFAULT_ALTS_MAX_CONCURRENT_HANDSHAKES); + } + + private void doHandshake() throws Exception { + // Capture the client frame and add to the server. + assertEquals(1, channel.outboundMessages().size()); + ByteBuf clientFrame = channel.readOutbound(); + assertTrue(serverHandshaker.processBytesFromPeer(clientFrame)); + + // Get the server response handshake frames. + ByteBuf serverFrame = channel.alloc().buffer(); + serverHandshaker.getBytesToSendToPeer(serverFrame); + channel.writeInbound(serverFrame); + + // Capture the next client frame and add to the server. + assertEquals(1, channel.outboundMessages().size()); + clientFrame = channel.readOutbound(); + assertTrue(serverHandshaker.processBytesFromPeer(clientFrame)); + + // Get the server response handshake frames. + serverFrame = channel.alloc().buffer(); + serverHandshaker.getBytesToSendToPeer(serverFrame); + channel.writeInbound(serverFrame); + + // Ensure that both sides have confirmed that the handshake has completed. + assertFalse(serverHandshaker.isInProgress()); + + if (caughtException != null) { + throw new RuntimeException(caughtException); + } + assertNotNull(grpcHandler.attrs); + } + + private CapturingGrpcHttp2ConnectionHandler capturingGrpcHandler() { + // Netty Boilerplate. We don't really need any of this, but there is a tight coupling + // between an Http2ConnectionHandler and its dependencies. + Http2Connection connection = new DefaultHttp2Connection(true); + Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(); + Http2FrameReader frameReader = new DefaultHttp2FrameReader(false); + DefaultHttp2ConnectionEncoder encoder = + new DefaultHttp2ConnectionEncoder(connection, frameWriter); + DefaultHttp2ConnectionDecoder decoder = + new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader); + + return new CapturingGrpcHttp2ConnectionHandler(decoder, encoder, new Http2Settings()); + } + + private final class CapturingGrpcHttp2ConnectionHandler extends GrpcHttp2ConnectionHandler { + + private Attributes attrs; + + private CapturingGrpcHttp2ConnectionHandler( + Http2ConnectionDecoder decoder, + Http2ConnectionEncoder encoder, + Http2Settings initialSettings) { + super(null, decoder, encoder, initialSettings, new NoopChannelLogger()); + } + + @Override + public void handleProtocolNegotiationCompleted( + Attributes attrs, + @SuppressWarnings("UnusedVariable") InternalChannelz.Security securityInfo) { + // If we are added to the pipeline, we need to remove ourselves. The HTTP2 handler + channel.pipeline().remove(this); + this.attrs = attrs; + } + } + + private static class DelegatingTsiHandshakerFactory implements TsiHandshakerFactory { + + private TsiHandshakerFactory delegate; + + DelegatingTsiHandshakerFactory(TsiHandshakerFactory delegate) { + this.delegate = delegate; + } + + @Override + public TsiHandshaker newHandshaker(String authority, ChannelLogger logger) { + return delegate.newHandshaker(authority, logger); + } + } + + private class DelegatingTsiHandshaker implements TsiHandshaker { + + private final TsiHandshaker delegate; + + DelegatingTsiHandshaker(TsiHandshaker delegate) { + this.delegate = delegate; + } + + @Override + public void getBytesToSendToPeer(ByteBuffer bytes) throws GeneralSecurityException { + delegate.getBytesToSendToPeer(bytes); + } + + @Override + public boolean processBytesFromPeer(ByteBuffer bytes) throws GeneralSecurityException { + return delegate.processBytesFromPeer(bytes); + } + + @Override + public boolean isInProgress() { + return delegate.isInProgress(); + } + + @Override + public TsiPeer extractPeer() throws GeneralSecurityException { + return delegate.extractPeer(); + } + + @Override + public Object extractPeerObject() throws GeneralSecurityException { + return delegate.extractPeerObject(); + } + + @Override + public TsiFrameProtector createFrameProtector(ByteBufAllocator alloc) { + InterceptingProtector protector = + new InterceptingProtector(delegate.createFrameProtector(alloc)); + protectors.add(protector); + return protector; + } + + @Override + public TsiFrameProtector createFrameProtector(int maxFrameSize, ByteBufAllocator alloc) { + InterceptingProtector protector = + new InterceptingProtector(delegate.createFrameProtector(maxFrameSize, alloc)); + protectors.add(protector); + return protector; + } + + @Override + public void close() { + delegate.close(); + } + } + + private static class InterceptingProtector implements TsiFrameProtector { + + private final TsiFrameProtector delegate; + final AtomicInteger flushes = new AtomicInteger(); + + InterceptingProtector(TsiFrameProtector delegate) { + this.delegate = delegate; + } + + @Override + public void protectFlush( + List unprotectedBufs, Consumer ctxWrite, ByteBufAllocator alloc) + throws GeneralSecurityException { + flushes.incrementAndGet(); + delegate.protectFlush(unprotectedBufs, ctxWrite, alloc); + } + + @Override + public void unprotect(ByteBuf in, List out, ByteBufAllocator alloc) + throws GeneralSecurityException { + delegate.unprotect(in, out, alloc); + } + + @Override + public void destroy() { + delegate.destroy(); + } + } + + /** Kicks off negotiation of the server. */ + private static final class KickNegotiationHandler extends ChannelInboundHandlerAdapter { + + private final ChannelHandler next; + + KickNegotiationHandler(ChannelHandler next) { + this.next = checkNotNull(next, "next"); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + ctx.pipeline().replace(ctx.name(), /*newName= */ null, next); + ctx.pipeline().fireUserEventTriggered(InternalProtocolNegotiationEvent.getDefault()); + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsTsiFrameProtectorTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsTsiFrameProtectorTest.java new file mode 100644 index 00000000000..cbda9dbcdc0 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsTsiFrameProtectorTest.java @@ -0,0 +1,491 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.alts.internal.ByteBufTestUtils.getDirectBuffer; +import static io.grpc.alts.internal.ByteBufTestUtils.getRandom; +import static io.grpc.alts.internal.ByteBufTestUtils.writeSlice; +import static org.junit.Assert.fail; + +import com.google.common.testing.GcFinalization; +import io.grpc.alts.internal.ByteBufTestUtils.RegisterRef; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetector.Level; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsTsiFrameProtector}. */ +@RunWith(JUnit4.class) +public class AltsTsiFrameProtectorTest { + private static final int FRAME_MIN_SIZE = + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + FakeChannelCrypter.getTagBytes(); + + private final List references = new ArrayList<>(); + private final RegisterRef ref = + new RegisterRef() { + @Override + public ByteBuf register(ByteBuf buf) { + if (buf != null) { + references.add(buf); + } + return buf; + } + }; + + @Before + public void setUp() { + ResourceLeakDetector.setLevel(Level.PARANOID); + } + + @After + public void teardown() { + for (ReferenceCounted reference : references) { + reference.release(); + } + references.clear(); + // Increase our chances to detect ByteBuf leaks. + GcFinalization.awaitFullGc(); + } + + @Test + public void parserHeader_frameLengthNegativeFails() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = getDirectBuffer(AltsTsiFrameProtector.getHeaderBytes(), ref); + in.writeIntLE(-1); + in.writeIntLE(6); + try { + unprotector.unprotect(in, out, alloc); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid header field: frame size too small"); + } + + unprotector.destroy(); + } + + @Test + public void parserHeader_frameTooSmall() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes(), ref); + in.writeIntLE(FRAME_MIN_SIZE - 1); + in.writeIntLE(6); + try { + unprotector.unprotect(in, out, alloc); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid header field: frame size too small"); + } + + unprotector.destroy(); + } + + @Test + public void parserHeader_frameTooLarge() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes(), ref); + in.writeIntLE( + AltsTsiFrameProtector.getLimitMaxAllowedFrameSize() + - AltsTsiFrameProtector.getHeaderLenFieldBytes() + + 1); + in.writeIntLE(6); + try { + unprotector.unprotect(in, out, alloc); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid header field: frame size too large"); + } + + unprotector.destroy(); + } + + @Test + public void parserHeader_frameTypeInvalid() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes(), ref); + in.writeIntLE(FRAME_MIN_SIZE); + in.writeIntLE(5); + try { + unprotector.unprotect(in, out, alloc); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid header field: frame type"); + } + + unprotector.destroy(); + } + + @Test + public void parserHeader_frameZeroOk() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes(), ref); + in.writeIntLE(FRAME_MIN_SIZE); + in.writeIntLE(6); + + unprotector.unprotect(in, out, alloc); + assertThat(in.readableBytes()).isEqualTo(0); + + unprotector.destroy(); + } + + @Test + public void parserHeader_EmptyUnprotectNoRetain() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf emptyBuf = getDirectBuffer(0, ref); + unprotector.unprotect(emptyBuf, out, alloc); + + assertThat(emptyBuf.refCnt()).isEqualTo(1); + + unprotector.destroy(); + } + + @Test + public void parserHeader_frameMaxOk() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes(), ref); + in.writeIntLE( + AltsTsiFrameProtector.getLimitMaxAllowedFrameSize() + - AltsTsiFrameProtector.getHeaderLenFieldBytes()); + in.writeIntLE(6); + + unprotector.unprotect(in, out, alloc); + assertThat(in.readableBytes()).isEqualTo(0); + + unprotector.destroy(); + } + + @Test + public void parserHeader_frameOkFragment() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes(), ref); + in.writeIntLE(FRAME_MIN_SIZE); + in.writeIntLE(6); + ByteBuf in1 = in.readSlice(AltsTsiFrameProtector.getHeaderBytes() - 1); + ByteBuf in2 = in.readSlice(1); + + unprotector.unprotect(in1, out, alloc); + assertThat(in1.readableBytes()).isEqualTo(0); + + unprotector.unprotect(in2, out, alloc); + assertThat(in2.readableBytes()).isEqualTo(0); + + unprotector.destroy(); + } + + @Test + public void parseHeader_frameFailFragment() throws GeneralSecurityException { + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf in = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes(), ref); + in.writeIntLE(FRAME_MIN_SIZE - 1); + in.writeIntLE(6); + ByteBuf in1 = in.readSlice(AltsTsiFrameProtector.getHeaderBytes() - 1); + ByteBuf in2 = in.readSlice(1); + + unprotector.unprotect(in1, out, alloc); + assertThat(in1.readableBytes()).isEqualTo(0); + + try { + unprotector.unprotect(in2, out, alloc); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid header field: frame size too small"); + } + + assertThat(in2.readableBytes()).isEqualTo(0); + + unprotector.destroy(); + } + + @Test + public void parseFrame_oneFrameNoFragment() throws GeneralSecurityException { + int payloadBytes = 1024; + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + ByteBuf plain = getRandom(payloadBytes, ref); + ByteBuf outFrame = + getDirectBuffer( + AltsTsiFrameProtector.getHeaderBytes() + + payloadBytes + + FakeChannelCrypter.getTagBytes(), + ref); + + outFrame.writeIntLE( + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + + payloadBytes + + FakeChannelCrypter.getTagBytes()); + outFrame.writeIntLE(6); + List framePlain = Collections.singletonList(plain); + ByteBuf frameOut = writeSlice(outFrame, payloadBytes + FakeChannelCrypter.getTagBytes()); + crypter.encrypt(frameOut, framePlain); + plain.readerIndex(0); + + unprotector.unprotect(outFrame, out, alloc); + assertThat(outFrame.readableBytes()).isEqualTo(0); + assertThat(out.size()).isEqualTo(1); + ByteBuf out1 = ref((ByteBuf) out.get(0)); + assertThat(out1).isEqualTo(plain); + + unprotector.destroy(); + } + + @Test + public void parseFrame_twoFramesNoFragment() throws GeneralSecurityException { + int payloadBytes = 1536; + int payloadBytes1 = 1024; + int payloadBytes2 = payloadBytes - payloadBytes1; + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + + ByteBuf plain = getRandom(payloadBytes, ref); + ByteBuf outFrame = + getDirectBuffer( + 2 * (AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes()) + + payloadBytes, + ref); + + outFrame.writeIntLE( + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + + payloadBytes1 + + FakeChannelCrypter.getTagBytes()); + outFrame.writeIntLE(6); + List framePlain1 = Collections.singletonList(plain.readSlice(payloadBytes1)); + ByteBuf frameOut1 = writeSlice(outFrame, payloadBytes1 + FakeChannelCrypter.getTagBytes()); + + outFrame.writeIntLE( + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + + payloadBytes2 + + FakeChannelCrypter.getTagBytes()); + outFrame.writeIntLE(6); + List framePlain2 = Collections.singletonList(plain); + ByteBuf frameOut2 = writeSlice(outFrame, payloadBytes2 + FakeChannelCrypter.getTagBytes()); + + crypter.encrypt(frameOut1, framePlain1); + crypter.encrypt(frameOut2, framePlain2); + plain.readerIndex(0); + + unprotector.unprotect(outFrame, out, alloc); + assertThat(out.size()).isEqualTo(1); + ByteBuf out1 = ref((ByteBuf) out.get(0)); + assertThat(out1).isEqualTo(plain); + assertThat(outFrame.refCnt()).isEqualTo(1); + assertThat(outFrame.readableBytes()).isEqualTo(0); + + unprotector.destroy(); + } + + @Test + public void parseFrame_twoFramesNoFragment_Leftover() throws GeneralSecurityException { + int payloadBytes = 1536; + int payloadBytes1 = 1024; + int payloadBytes2 = payloadBytes - payloadBytes1; + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + + ByteBuf plain = getRandom(payloadBytes, ref); + ByteBuf protectedBuf = + getDirectBuffer( + 2 * (AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes()) + + payloadBytes + + AltsTsiFrameProtector.getHeaderBytes(), + ref); + + protectedBuf.writeIntLE( + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + + payloadBytes1 + + FakeChannelCrypter.getTagBytes()); + protectedBuf.writeIntLE(6); + List framePlain1 = Collections.singletonList(plain.readSlice(payloadBytes1)); + ByteBuf frameOut1 = writeSlice(protectedBuf, payloadBytes1 + FakeChannelCrypter.getTagBytes()); + + protectedBuf.writeIntLE( + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + + payloadBytes2 + + FakeChannelCrypter.getTagBytes()); + protectedBuf.writeIntLE(6); + List framePlain2 = Collections.singletonList(plain); + ByteBuf frameOut2 = writeSlice(protectedBuf, payloadBytes2 + FakeChannelCrypter.getTagBytes()); + // This is an invalid header length field, make sure it triggers an error + // when the remainder of the header is given. + protectedBuf.writeIntLE((byte) -1); + + crypter.encrypt(frameOut1, framePlain1); + crypter.encrypt(frameOut2, framePlain2); + plain.readerIndex(0); + + unprotector.unprotect(protectedBuf, out, alloc); + assertThat(out.size()).isEqualTo(1); + ByteBuf out1 = ref((ByteBuf) out.get(0)); + assertThat(out1).isEqualTo(plain); + + // The protectedBuf is buffered inside the unprotector. + assertThat(protectedBuf.readableBytes()).isEqualTo(0); + assertThat(protectedBuf.refCnt()).isEqualTo(2); + + protectedBuf.writeIntLE(6); + try { + unprotector.unprotect(protectedBuf, out, alloc); + fail("Exception expected"); + } catch (IllegalArgumentException ex) { + assertThat(ex).hasMessageThat().contains("Invalid header field: frame size too small"); + } + + unprotector.destroy(); + + // Make sure that unprotector does not hold onto buffered ByteBuf instance after destroy. + assertThat(protectedBuf.refCnt()).isEqualTo(1); + + // Make sure that destroying twice does not throw. + unprotector.destroy(); + } + + @Test + public void parseFrame_twoFramesFragmentSecond() throws GeneralSecurityException { + int payloadBytes = 1536; + int payloadBytes1 = 1024; + int payloadBytes2 = payloadBytes - payloadBytes1; + ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; + List out = new ArrayList<>(); + FakeChannelCrypter crypter = new FakeChannelCrypter(); + AltsTsiFrameProtector.Unprotector unprotector = + new AltsTsiFrameProtector.Unprotector(crypter, alloc); + + ByteBuf plain = getRandom(payloadBytes, ref); + ByteBuf protectedBuf = + getDirectBuffer( + 2 * (AltsTsiFrameProtector.getHeaderBytes() + FakeChannelCrypter.getTagBytes()) + + payloadBytes + + AltsTsiFrameProtector.getHeaderBytes(), + ref); + + protectedBuf.writeIntLE( + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + + payloadBytes1 + + FakeChannelCrypter.getTagBytes()); + protectedBuf.writeIntLE(6); + List framePlain1 = Collections.singletonList(plain.readSlice(payloadBytes1)); + ByteBuf frameOut1 = writeSlice(protectedBuf, payloadBytes1 + FakeChannelCrypter.getTagBytes()); + + protectedBuf.writeIntLE( + AltsTsiFrameProtector.getHeaderTypeFieldBytes() + + payloadBytes2 + + FakeChannelCrypter.getTagBytes()); + protectedBuf.writeIntLE(6); + List framePlain2 = Collections.singletonList(plain); + ByteBuf frameOut2 = writeSlice(protectedBuf, payloadBytes2 + FakeChannelCrypter.getTagBytes()); + + crypter.encrypt(frameOut1, framePlain1); + crypter.encrypt(frameOut2, framePlain2); + plain.readerIndex(0); + + unprotector.unprotect( + protectedBuf.readSlice( + payloadBytes + + AltsTsiFrameProtector.getHeaderBytes() + + FakeChannelCrypter.getTagBytes() + + AltsTsiFrameProtector.getHeaderBytes()), + out, + alloc); + assertThat(out.size()).isEqualTo(1); + ByteBuf out1 = ref((ByteBuf) out.get(0)); + assertThat(out1).isEqualTo(plain.readSlice(payloadBytes1)); + assertThat(protectedBuf.refCnt()).isEqualTo(2); + + unprotector.unprotect(protectedBuf, out, alloc); + assertThat(out.size()).isEqualTo(2); + ByteBuf out2 = ref((ByteBuf) out.get(1)); + assertThat(out2).isEqualTo(plain); + assertThat(protectedBuf.refCnt()).isEqualTo(1); + + unprotector.destroy(); + } + + private ByteBuf ref(ByteBuf buf) { + if (buf != null) { + references.add(buf); + } + return buf; + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsTsiHandshakerTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsTsiHandshakerTest.java new file mode 100644 index 00000000000..ae1696401be --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsTsiHandshakerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import io.grpc.internal.TestUtils.NoopChannelLogger; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentMatchers; + +/** Unit tests for {@link AltsTsiHandshaker}. */ +@RunWith(JUnit4.class) +public class AltsTsiHandshakerTest { + private static final String TEST_KEY_DATA = "super secret 123"; + private static final String TEST_APPLICATION_PROTOCOL = "grpc"; + private static final String TEST_RECORD_PROTOCOL = "ALTSRP_GCM_AES128"; + private static final String TEST_CLIENT_SERVICE_ACCOUNT = "client@developer.gserviceaccount.com"; + private static final String TEST_SERVER_SERVICE_ACCOUNT = "server@developer.gserviceaccount.com"; + private static final int OUT_FRAME_SIZE = 100; + private static final int TRANSPORT_BUFFER_SIZE = 200; + private static final int TEST_MAX_RPC_VERSION_MAJOR = 3; + private static final int TEST_MAX_RPC_VERSION_MINOR = 2; + private static final int TEST_MIN_RPC_VERSION_MAJOR = 2; + private static final int TEST_MIN_RPC_VERSION_MINOR = 1; + private static final RpcProtocolVersions TEST_RPC_PROTOCOL_VERSIONS = + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion( + RpcProtocolVersions.Version.newBuilder() + .setMajor(TEST_MAX_RPC_VERSION_MAJOR) + .setMinor(TEST_MAX_RPC_VERSION_MINOR) + .build()) + .setMinRpcVersion( + RpcProtocolVersions.Version.newBuilder() + .setMajor(TEST_MIN_RPC_VERSION_MAJOR) + .setMinor(TEST_MIN_RPC_VERSION_MINOR) + .build()) + .build(); + + private AltsHandshakerClient mockClient; + private AltsHandshakerClient mockServer; + private AltsTsiHandshaker handshakerClient; + private AltsTsiHandshaker handshakerServer; + + @Before + public void setUp() throws Exception { + mockClient = mock(AltsHandshakerClient.class); + mockServer = mock(AltsHandshakerClient.class); + NoopChannelLogger channelLogger = new NoopChannelLogger(); + handshakerClient = new AltsTsiHandshaker(true, mockClient, channelLogger); + handshakerServer = new AltsTsiHandshaker(false, mockServer, channelLogger); + } + + private HandshakerResult getHandshakerResult(boolean isClient) { + HandshakerResult.Builder builder = + HandshakerResult.newBuilder() + .setApplicationProtocol(TEST_APPLICATION_PROTOCOL) + .setRecordProtocol(TEST_RECORD_PROTOCOL) + .setKeyData(ByteString.copyFromUtf8(TEST_KEY_DATA)) + .setPeerRpcVersions(TEST_RPC_PROTOCOL_VERSIONS); + if (isClient) { + builder.setPeerIdentity( + Identity.newBuilder().setServiceAccount(TEST_SERVER_SERVICE_ACCOUNT).build()); + builder.setLocalIdentity( + Identity.newBuilder().setServiceAccount(TEST_CLIENT_SERVICE_ACCOUNT).build()); + } else { + builder.setPeerIdentity( + Identity.newBuilder().setServiceAccount(TEST_CLIENT_SERVICE_ACCOUNT).build()); + builder.setLocalIdentity( + Identity.newBuilder().setServiceAccount(TEST_SERVER_SERVICE_ACCOUNT).build()); + } + return builder.build(); + } + + @Test + public void processBytesFromPeerFalseStart() throws Exception { + verify(mockClient, never()).startClientHandshake(); + verify(mockClient, never()).startServerHandshake(ArgumentMatchers.any()); + verify(mockClient, never()).next(ArgumentMatchers.any()); + + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + assertTrue(handshakerClient.processBytesFromPeer(transportBuffer)); + } + + @Test + public void processBytesFromPeerStartServer() throws Exception { + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + ByteBuffer outputFrame = ByteBuffer.allocate(OUT_FRAME_SIZE); + verify(mockServer, never()).startClientHandshake(); + verify(mockServer, never()).next(ArgumentMatchers.any()); + // Mock transport buffer all consumed by processBytesFromPeer and there is an output frame. + ((Buffer) transportBuffer).position(transportBuffer.limit()); + when(mockServer.startServerHandshake(transportBuffer)).thenReturn(outputFrame); + when(mockServer.isFinished()).thenReturn(false); + + assertTrue(handshakerServer.processBytesFromPeer(transportBuffer)); + } + + @Test + public void processBytesFromPeerStartServerEmptyOutput() throws Exception { + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + ByteBuffer emptyOutputFrame = ByteBuffer.allocate(0); + verify(mockServer, never()).startClientHandshake(); + verify(mockServer, never()).next(ArgumentMatchers.any()); + // Mock transport buffer all consumed by processBytesFromPeer and output frame is empty. + // Expect processBytesFromPeer return False, because more data are needed from the peer. + ((Buffer) transportBuffer).position(transportBuffer.limit()); + when(mockServer.startServerHandshake(transportBuffer)).thenReturn(emptyOutputFrame); + when(mockServer.isFinished()).thenReturn(false); + + assertFalse(handshakerServer.processBytesFromPeer(transportBuffer)); + } + + @Test + public void processBytesFromPeerStartServerFinished() throws Exception { + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + ByteBuffer outputFrame = ByteBuffer.allocate(OUT_FRAME_SIZE); + verify(mockServer, never()).startClientHandshake(); + verify(mockServer, never()).next(ArgumentMatchers.any()); + // Mock handshake complete after processBytesFromPeer. + when(mockServer.startServerHandshake(transportBuffer)).thenReturn(outputFrame); + when(mockServer.isFinished()).thenReturn(true); + + assertTrue(handshakerServer.processBytesFromPeer(transportBuffer)); + } + + @Test + public void processBytesFromPeerNoBytesConsumed() throws Exception { + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + ByteBuffer emptyOutputFrame = ByteBuffer.allocate(0); + verify(mockServer, never()).startClientHandshake(); + verify(mockServer, never()).next(ArgumentMatchers.any()); + when(mockServer.startServerHandshake(transportBuffer)).thenReturn(emptyOutputFrame); + when(mockServer.isFinished()).thenReturn(false); + + try { + assertTrue(handshakerServer.processBytesFromPeer(transportBuffer)); + fail("Expected IllegalStateException"); + } catch (IllegalStateException expected) { + assertEquals("Handshaker did not consume any bytes.", expected.getMessage()); + } + } + + @Test + public void processBytesFromPeerClientNext() throws Exception { + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + ByteBuffer outputFrame = ByteBuffer.allocate(OUT_FRAME_SIZE); + verify(mockClient, never()).startServerHandshake(ArgumentMatchers.any()); + when(mockClient.startClientHandshake()).thenReturn(outputFrame); + when(mockClient.next(transportBuffer)).thenReturn(outputFrame); + when(mockClient.isFinished()).thenReturn(false); + + handshakerClient.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).position(transportBuffer.limit()); + assertFalse(handshakerClient.processBytesFromPeer(transportBuffer)); + } + + @Test + public void processBytesFromPeerClientNextFinished() throws Exception { + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + ByteBuffer outputFrame = ByteBuffer.allocate(OUT_FRAME_SIZE); + verify(mockClient, never()).startServerHandshake(ArgumentMatchers.any()); + when(mockClient.startClientHandshake()).thenReturn(outputFrame); + when(mockClient.next(transportBuffer)).thenReturn(outputFrame); + when(mockClient.isFinished()).thenReturn(true); + + handshakerClient.getBytesToSendToPeer(transportBuffer); + assertTrue(handshakerClient.processBytesFromPeer(transportBuffer)); + } + + @Test + public void extractPeerFailure() throws Exception { + when(mockClient.isFinished()).thenReturn(false); + + try { + handshakerClient.extractPeer(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException expected) { + assertEquals("Handshake is not complete.", expected.getMessage()); + } + } + + @Test + public void extractPeerObjectFailure() throws Exception { + when(mockClient.isFinished()).thenReturn(false); + + try { + handshakerClient.extractPeerObject(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException expected) { + assertEquals("Handshake is not complete.", expected.getMessage()); + } + } + + @Test + public void extractClientPeerSuccess() throws Exception { + ByteBuffer outputFrame = ByteBuffer.allocate(OUT_FRAME_SIZE); + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + when(mockClient.startClientHandshake()).thenReturn(outputFrame); + when(mockClient.isFinished()).thenReturn(true); + when(mockClient.getResult()).thenReturn(getHandshakerResult(/* isClient = */ true)); + + handshakerClient.getBytesToSendToPeer(transportBuffer); + TsiPeer clientPeer = handshakerClient.extractPeer(); + + assertEquals(1, clientPeer.getProperties().size()); + assertEquals( + TEST_SERVER_SERVICE_ACCOUNT, + clientPeer.getProperty(AltsTsiHandshaker.TSI_SERVICE_ACCOUNT_PEER_PROPERTY).getValue()); + + AltsInternalContext clientContext = (AltsInternalContext) handshakerClient.extractPeerObject(); + assertEquals(TEST_APPLICATION_PROTOCOL, clientContext.getApplicationProtocol()); + assertEquals(TEST_RECORD_PROTOCOL, clientContext.getRecordProtocol()); + assertEquals(TEST_SERVER_SERVICE_ACCOUNT, clientContext.getPeerServiceAccount()); + assertEquals(TEST_CLIENT_SERVICE_ACCOUNT, clientContext.getLocalServiceAccount()); + assertEquals(TEST_RPC_PROTOCOL_VERSIONS, clientContext.getPeerRpcVersions()); + } + + @Test + public void extractServerPeerSuccess() throws Exception { + ByteBuffer outputFrame = ByteBuffer.allocate(OUT_FRAME_SIZE); + ByteBuffer transportBuffer = ByteBuffer.allocate(TRANSPORT_BUFFER_SIZE); + when(mockServer.startServerHandshake(ArgumentMatchers.any())) + .thenReturn(outputFrame); + when(mockServer.isFinished()).thenReturn(true); + when(mockServer.getResult()).thenReturn(getHandshakerResult(/* isClient = */ false)); + + handshakerServer.processBytesFromPeer(transportBuffer); + handshakerServer.getBytesToSendToPeer(transportBuffer); + TsiPeer serverPeer = handshakerServer.extractPeer(); + + assertEquals(1, serverPeer.getProperties().size()); + assertEquals( + TEST_CLIENT_SERVICE_ACCOUNT, + serverPeer.getProperty(AltsTsiHandshaker.TSI_SERVICE_ACCOUNT_PEER_PROPERTY).getValue()); + + AltsInternalContext serverContext = (AltsInternalContext) handshakerServer.extractPeerObject(); + assertEquals(TEST_APPLICATION_PROTOCOL, serverContext.getApplicationProtocol()); + assertEquals(TEST_RECORD_PROTOCOL, serverContext.getRecordProtocol()); + assertEquals(TEST_CLIENT_SERVICE_ACCOUNT, serverContext.getPeerServiceAccount()); + assertEquals(TEST_SERVER_SERVICE_ACCOUNT, serverContext.getLocalServiceAccount()); + assertEquals(TEST_RPC_PROTOCOL_VERSIONS, serverContext.getPeerRpcVersions()); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsTsiTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsTsiTest.java new file mode 100644 index 00000000000..cb39abb9ddc --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/AltsTsiTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static org.junit.Assert.assertEquals; + +import com.google.common.testing.GcFinalization; +import io.grpc.alts.internal.ByteBufTestUtils.RegisterRef; +import io.grpc.alts.internal.TsiTest.Handshakers; +import io.grpc.internal.TestUtils.NoopChannelLogger; +import io.netty.buffer.ByteBuf; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetector.Level; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link AltsTsiHandshaker}. */ +@RunWith(JUnit4.class) +public class AltsTsiTest { + private static final int OVERHEAD = + FakeChannelCrypter.getTagBytes() + AltsTsiFrameProtector.getHeaderBytes(); + + private final List references = new ArrayList<>(); + private AltsHandshakerClient client; + private AltsHandshakerClient server; + private final RegisterRef ref = + new RegisterRef() { + @Override + public ByteBuf register(ByteBuf buf) { + if (buf != null) { + references.add(buf); + } + return buf; + } + }; + + @Before + public void setUp() throws Exception { + ResourceLeakDetector.setLevel(Level.PARANOID); + // Use MockAltsHandshakerStub for all the tests. + AltsHandshakerOptions handshakerOptions = new AltsHandshakerOptions(null); + MockAltsHandshakerStub clientStub = new MockAltsHandshakerStub(); + MockAltsHandshakerStub serverStub = new MockAltsHandshakerStub(); + NoopChannelLogger channelLogger = new NoopChannelLogger(); + client = new AltsHandshakerClient(clientStub, handshakerOptions, channelLogger); + server = new AltsHandshakerClient(serverStub, handshakerOptions, channelLogger); + } + + @After + public void tearDown() { + for (ReferenceCounted reference : references) { + reference.release(); + } + references.clear(); + // Increase our chances to detect ByteBuf leaks. + GcFinalization.awaitFullGc(); + } + + private Handshakers newHandshakers() { + NoopChannelLogger channelLogger = new NoopChannelLogger(); + TsiHandshaker clientHandshaker = new AltsTsiHandshaker(true, client, channelLogger); + TsiHandshaker serverHandshaker = new AltsTsiHandshaker(false, server, channelLogger); + return new Handshakers(clientHandshaker, serverHandshaker); + } + + @Test + public void verifyHandshakePeer() throws Exception { + Handshakers handshakers = newHandshakers(); + TsiTest.performHandshake(TsiTest.getDefaultTransportBufferSize(), handshakers); + TsiPeer clientPeer = handshakers.getClient().extractPeer(); + assertEquals(1, clientPeer.getProperties().size()); + assertEquals( + MockAltsHandshakerResp.getTestPeerAccount(), + clientPeer.getProperty("service_account").getValue()); + TsiPeer serverPeer = handshakers.getServer().extractPeer(); + assertEquals(1, serverPeer.getProperties().size()); + assertEquals( + MockAltsHandshakerResp.getTestPeerAccount(), + serverPeer.getProperty("service_account").getValue()); + } + + @Test + public void handshake() throws GeneralSecurityException { + TsiTest.handshakeTest(newHandshakers()); + } + + @Test + public void handshakeSmallBuffer() throws GeneralSecurityException { + TsiTest.handshakeSmallBufferTest(newHandshakers()); + } + + @Test + public void pingPong() throws GeneralSecurityException { + TsiTest.pingPongTest(newHandshakers(), ref); + } + + @Test + public void pingPongExactFrameSize() throws GeneralSecurityException { + TsiTest.pingPongExactFrameSizeTest(newHandshakers(), ref); + } + + @Test + public void pingPongSmallBuffer() throws GeneralSecurityException { + TsiTest.pingPongSmallBufferTest(newHandshakers(), ref); + } + + @Test + public void pingPongSmallFrame() throws GeneralSecurityException { + TsiTest.pingPongSmallFrameTest(OVERHEAD, newHandshakers(), ref); + } + + @Test + public void pingPongSmallFrameSmallBuffer() throws GeneralSecurityException { + TsiTest.pingPongSmallFrameSmallBufferTest(OVERHEAD, newHandshakers(), ref); + } + + @Test + public void corruptedCounter() throws GeneralSecurityException { + TsiTest.corruptedCounterTest(newHandshakers(), ref); + } + + @Test + public void corruptedCiphertext() throws GeneralSecurityException { + TsiTest.corruptedCiphertextTest(newHandshakers(), ref); + } + + @Test + public void corruptedTag() throws GeneralSecurityException { + TsiTest.corruptedTagTest(newHandshakers(), ref); + } + + @Test + public void reflectedCiphertext() throws GeneralSecurityException { + TsiTest.reflectedCiphertextTest(newHandshakers(), ref); + } + + private static class MockAltsHandshakerStub extends AltsHandshakerStub { + private boolean started = false; + + @Override + public HandshakerResp send(HandshakerReq req) { + if (started) { + // Expect handshake next message. + if (req.getReqOneofCase().getNumber() != 3) { + return MockAltsHandshakerResp.getErrorResponse(); + } + return MockAltsHandshakerResp.getFinishedResponse(req.getNext().getInBytes().size()); + } else { + List recordProtocols; + int bytesConsumed = 0; + switch (req.getReqOneofCase().getNumber()) { + case 1: + recordProtocols = req.getClientStart().getRecordProtocolsList(); + break; + case 2: + recordProtocols = + req.getServerStart() + .getHandshakeParametersMap() + .get(HandshakeProtocol.ALTS.getNumber()) + .getRecordProtocolsList(); + bytesConsumed = req.getServerStart().getInBytes().size(); + break; + default: + return MockAltsHandshakerResp.getErrorResponse(); + } + if (recordProtocols.isEmpty()) { + return MockAltsHandshakerResp.getErrorResponse(); + } + started = true; + return MockAltsHandshakerResp.getOkResponse(bytesConsumed); + } + } + + @Override + public void close() {} + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/BufUnwrapperTest.java b/alts/src/test/java/io/grpc/alts/internal/BufUnwrapperTest.java new file mode 100644 index 00000000000..67f318e8611 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/BufUnwrapperTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static org.junit.Assert.assertEquals; + +import com.google.common.truth.Truth; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BufUnwrapperTest { + + private final ByteBufAllocator alloc = UnpooledByteBufAllocator.DEFAULT; + + @Test + public void closeEmptiesBuffers() { + BufUnwrapper unwrapper = new BufUnwrapper(); + ByteBuf buf = alloc.buffer(); + try { + ByteBuffer[] readableBufs = unwrapper.readableNioBuffers(buf); + ByteBuffer[] writableBufs = unwrapper.writableNioBuffers(buf); + Truth.assertThat(readableBufs).hasLength(1); + Truth.assertThat(readableBufs[0]).isNotNull(); + Truth.assertThat(writableBufs).hasLength(1); + Truth.assertThat(writableBufs[0]).isNotNull(); + + unwrapper.close(); + + Truth.assertThat(readableBufs[0]).isNull(); + Truth.assertThat(writableBufs[0]).isNull(); + } finally { + buf.release(); + } + } + + @Test + public void readableNioBuffers_worksWithNormal() { + ByteBuf buf = alloc.buffer(1).writeByte('a'); + try (BufUnwrapper unwrapper = new BufUnwrapper()) { + ByteBuffer[] internalBufs = unwrapper.readableNioBuffers(buf); + Truth.assertThat(internalBufs).hasLength(1); + + assertEquals('a', internalBufs[0].get(0)); + } finally { + buf.release(); + } + } + + @Test + public void readableNioBuffers_worksWithComposite() { + CompositeByteBuf buf = alloc.compositeBuffer(); + buf.addComponent(true, alloc.buffer(1).writeByte('a')); + try (BufUnwrapper unwrapper = new BufUnwrapper()) { + ByteBuffer[] internalBufs = unwrapper.readableNioBuffers(buf); + Truth.assertThat(internalBufs).hasLength(1); + + assertEquals('a', internalBufs[0].get(0)); + } finally { + buf.release(); + } + } + + @Test + public void writableNioBuffers_indexesPreserved() { + ByteBuf buf = alloc.buffer(1); + int ridx = buf.readerIndex(); + int widx = buf.writerIndex(); + int cap = buf.capacity(); + try (BufUnwrapper unwrapper = new BufUnwrapper()) { + ByteBuffer[] internalBufs = unwrapper.writableNioBuffers(buf); + Truth.assertThat(internalBufs).hasLength(1); + + internalBufs[0].put((byte) 'a'); + + assertEquals(ridx, buf.readerIndex()); + assertEquals(widx, buf.writerIndex()); + assertEquals(cap, buf.capacity()); + } finally { + buf.release(); + } + } + + @Test + public void writableNioBuffers_worksWithNormal() { + ByteBuf buf = alloc.buffer(1); + try (BufUnwrapper unwrapper = new BufUnwrapper()) { + ByteBuffer[] internalBufs = unwrapper.writableNioBuffers(buf); + Truth.assertThat(internalBufs).hasLength(1); + + internalBufs[0].put((byte) 'a'); + + buf.writerIndex(1); + assertEquals('a', buf.readByte()); + } finally { + buf.release(); + } + } + + @Test + public void writableNioBuffers_worksWithComposite() { + CompositeByteBuf buf = alloc.compositeBuffer(); + buf.addComponent(alloc.buffer(1)); + buf.capacity(1); + try (BufUnwrapper unwrapper = new BufUnwrapper()) { + ByteBuffer[] internalBufs = unwrapper.writableNioBuffers(buf); + Truth.assertThat(internalBufs).hasLength(1); + + internalBufs[0].put((byte) 'a'); + + buf.writerIndex(1); + assertEquals('a', buf.readByte()); + } finally { + buf.release(); + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/ByteBufTestUtils.java b/alts/src/test/java/io/grpc/alts/internal/ByteBufTestUtils.java new file mode 100644 index 00000000000..0d0f13c9cc4 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/ByteBufTestUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import com.google.common.base.Preconditions; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public final class ByteBufTestUtils { + public interface RegisterRef { + ByteBuf register(ByteBuf buf); + } + + private static final Random random = new SecureRandom(); + + // The {@code ref} argument can be used to register the buffer for {@code release}. + // TODO: allow the allocator to be passed in. + public static ByteBuf getDirectBuffer(int len, RegisterRef ref) { + return ref.register(Unpooled.directBuffer(len)); + } + + /** Get random bytes. */ + public static ByteBuf getRandom(int len, RegisterRef ref) { + ByteBuf buf = getDirectBuffer(len, ref); + byte[] bytes = new byte[len]; + random.nextBytes(bytes); + buf.writeBytes(bytes); + return buf; + } + + /** Fragment byte buffer into multiple pieces. */ + public static List fragmentByteBuf(ByteBuf in, int num, RegisterRef ref) { + ByteBuf buf = in.slice(); + Preconditions.checkArgument(num > 0); + List fragmentedBufs = new ArrayList<>(num); + int fragmentSize = buf.readableBytes() / num; + while (buf.isReadable()) { + int readBytes = num == 0 ? buf.readableBytes() : fragmentSize; + ByteBuf tmpBuf = getDirectBuffer(readBytes, ref); + tmpBuf.writeBytes(buf, readBytes); + fragmentedBufs.add(tmpBuf); + num--; + } + return fragmentedBufs; + } + + static ByteBuf writeSlice(ByteBuf in, int len) { + Preconditions.checkArgument(len <= in.writableBytes()); + ByteBuf out = in.slice(in.writerIndex(), len); + in.writerIndex(in.writerIndex() + len); + return out.writerIndex(0); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/ChannelCrypterNettyTestBase.java b/alts/src/test/java/io/grpc/alts/internal/ChannelCrypterNettyTestBase.java new file mode 100644 index 00000000000..cab0765e7d7 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/ChannelCrypterNettyTestBase.java @@ -0,0 +1,226 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.alts.internal.ByteBufTestUtils.getDirectBuffer; +import static io.grpc.alts.internal.ByteBufTestUtils.getRandom; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; + +import io.grpc.alts.internal.ByteBufTestUtils.RegisterRef; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCounted; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.crypto.AEADBadTagException; +import org.junit.Test; + +/** Abstract class for unit tests of {@link ChannelCrypterNetty}. */ +public abstract class ChannelCrypterNettyTestBase { + private static final String DECRYPTION_FAILURE_MESSAGE_RE = "Tag mismatch|BAD_DECRYPT"; + + protected final List references = new ArrayList<>(); + public ChannelCrypterNetty client; + public ChannelCrypterNetty server; + private final RegisterRef ref = + new RegisterRef() { + @Override + public ByteBuf register(ByteBuf buf) { + if (buf != null) { + references.add(buf); + } + return buf; + } + }; + + static final class FrameEncrypt { + List plain; + ByteBuf out; + } + + static final class FrameDecrypt { + List ciphertext; + ByteBuf out; + ByteBuf tag; + } + + FrameEncrypt createFrameEncrypt(String message) { + byte[] messageBytes = message.getBytes(UTF_8); + FrameEncrypt frame = new FrameEncrypt(); + ByteBuf plain = getDirectBuffer(messageBytes.length, ref); + plain.writeBytes(messageBytes); + frame.plain = Collections.singletonList(plain); + frame.out = getDirectBuffer(messageBytes.length + client.getSuffixLength(), ref); + return frame; + } + + FrameDecrypt frameDecryptOfEncrypt(FrameEncrypt frameEncrypt) { + int tagLen = client.getSuffixLength(); + FrameDecrypt frameDecrypt = new FrameDecrypt(); + ByteBuf out = frameEncrypt.out; + frameDecrypt.ciphertext = + Collections.singletonList(out.slice(out.readerIndex(), out.readableBytes() - tagLen)); + frameDecrypt.tag = out.slice(out.readerIndex() + out.readableBytes() - tagLen, tagLen); + frameDecrypt.out = getDirectBuffer(out.readableBytes(), ref); + return frameDecrypt; + } + + @Test + public void encryptDecrypt() throws GeneralSecurityException { + String message = "Hello world"; + FrameEncrypt frameEncrypt = createFrameEncrypt(message); + client.encrypt(frameEncrypt.out, frameEncrypt.plain); + FrameDecrypt frameDecrypt = frameDecryptOfEncrypt(frameEncrypt); + + server.decrypt(frameDecrypt.out, frameDecrypt.tag, frameDecrypt.ciphertext); + assertThat(frameEncrypt.plain.get(0).slice(0, frameDecrypt.out.readableBytes())) + .isEqualTo(frameDecrypt.out); + } + + @Test + public void encryptDecryptLarge() throws GeneralSecurityException { + FrameEncrypt frameEncrypt = new FrameEncrypt(); + ByteBuf plain = getRandom(17 * 1024, ref); + frameEncrypt.plain = Collections.singletonList(plain); + frameEncrypt.out = getDirectBuffer(plain.readableBytes() + client.getSuffixLength(), ref); + + client.encrypt(frameEncrypt.out, frameEncrypt.plain); + FrameDecrypt frameDecrypt = frameDecryptOfEncrypt(frameEncrypt); + + // Call decrypt overload that takes ciphertext and tag. + server.decrypt(frameDecrypt.out, frameEncrypt.out); + assertThat(frameEncrypt.plain.get(0).slice(0, frameDecrypt.out.readableBytes())) + .isEqualTo(frameDecrypt.out); + } + + @Test + public void encryptDecryptMultiple() throws GeneralSecurityException { + String message = "Hello world"; + for (int i = 0; i < 512; ++i) { + FrameEncrypt frameEncrypt = createFrameEncrypt(message); + client.encrypt(frameEncrypt.out, frameEncrypt.plain); + FrameDecrypt frameDecrypt = frameDecryptOfEncrypt(frameEncrypt); + + server.decrypt(frameDecrypt.out, frameDecrypt.tag, frameDecrypt.ciphertext); + assertThat(frameEncrypt.plain.get(0).slice(0, frameDecrypt.out.readableBytes())) + .isEqualTo(frameDecrypt.out); + } + } + + @Test + public void encryptDecryptComposite() throws GeneralSecurityException { + String message = "Hello world"; + int lastLen = 2; + byte[] messageBytes = message.getBytes(UTF_8); + FrameEncrypt frameEncrypt = new FrameEncrypt(); + ByteBuf plain1 = getDirectBuffer(messageBytes.length - lastLen, ref); + ByteBuf plain2 = getDirectBuffer(lastLen, ref); + plain1.writeBytes(messageBytes, 0, messageBytes.length - lastLen); + plain2.writeBytes(messageBytes, messageBytes.length - lastLen, lastLen); + ByteBuf plain = Unpooled.wrappedBuffer(plain1, plain2); + frameEncrypt.plain = Collections.singletonList(plain); + frameEncrypt.out = getDirectBuffer(messageBytes.length + client.getSuffixLength(), ref); + + client.encrypt(frameEncrypt.out, frameEncrypt.plain); + + int tagLen = client.getSuffixLength(); + FrameDecrypt frameDecrypt = new FrameDecrypt(); + ByteBuf out = frameEncrypt.out; + int outLen = out.readableBytes(); + ByteBuf cipher1 = getDirectBuffer(outLen - lastLen - tagLen, ref); + ByteBuf cipher2 = getDirectBuffer(lastLen, ref); + cipher1.writeBytes(out, 0, outLen - lastLen - tagLen); + cipher2.writeBytes(out, outLen - tagLen - lastLen, lastLen); + ByteBuf cipher = Unpooled.wrappedBuffer(cipher1, cipher2); + frameDecrypt.ciphertext = Collections.singletonList(cipher); + frameDecrypt.tag = out.slice(out.readerIndex() + out.readableBytes() - tagLen, tagLen); + frameDecrypt.out = getDirectBuffer(out.readableBytes(), ref); + + server.decrypt(frameDecrypt.out, frameDecrypt.tag, frameDecrypt.ciphertext); + assertThat(frameEncrypt.plain.get(0).slice(0, frameDecrypt.out.readableBytes())) + .isEqualTo(frameDecrypt.out); + } + + @Test + public void reflection() throws GeneralSecurityException { + String message = "Hello world"; + FrameEncrypt frameEncrypt = createFrameEncrypt(message); + client.encrypt(frameEncrypt.out, frameEncrypt.plain); + FrameDecrypt frameDecrypt = frameDecryptOfEncrypt(frameEncrypt); + try { + client.decrypt(frameDecrypt.out, frameDecrypt.tag, frameDecrypt.ciphertext); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_MESSAGE_RE); + } + } + + @Test + public void skipMessage() throws GeneralSecurityException { + String message = "Hello world"; + FrameEncrypt frameEncrypt1 = createFrameEncrypt(message); + client.encrypt(frameEncrypt1.out, frameEncrypt1.plain); + FrameEncrypt frameEncrypt2 = createFrameEncrypt(message); + client.encrypt(frameEncrypt2.out, frameEncrypt2.plain); + FrameDecrypt frameDecrypt = frameDecryptOfEncrypt(frameEncrypt2); + + try { + client.decrypt(frameDecrypt.out, frameDecrypt.tag, frameDecrypt.ciphertext); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_MESSAGE_RE); + } + } + + @Test + public void corruptMessage() throws GeneralSecurityException { + String message = "Hello world"; + FrameEncrypt frameEncrypt = createFrameEncrypt(message); + client.encrypt(frameEncrypt.out, frameEncrypt.plain); + FrameDecrypt frameDecrypt = frameDecryptOfEncrypt(frameEncrypt); + frameEncrypt.out.setByte(3, frameEncrypt.out.getByte(3) + 1); + + try { + client.decrypt(frameDecrypt.out, frameDecrypt.tag, frameDecrypt.ciphertext); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_MESSAGE_RE); + } + } + + @Test + public void replayMessage() throws GeneralSecurityException { + String message = "Hello world"; + FrameEncrypt frameEncrypt = createFrameEncrypt(message); + client.encrypt(frameEncrypt.out, frameEncrypt.plain); + FrameDecrypt frameDecrypt1 = frameDecryptOfEncrypt(frameEncrypt); + FrameDecrypt frameDecrypt2 = frameDecryptOfEncrypt(frameEncrypt); + + server.decrypt(frameDecrypt1.out, frameDecrypt1.tag, frameDecrypt1.ciphertext); + + try { + server.decrypt(frameDecrypt2.out, frameDecrypt2.tag, frameDecrypt2.ciphertext); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_MESSAGE_RE); + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/FakeChannelCrypter.java b/alts/src/test/java/io/grpc/alts/internal/FakeChannelCrypter.java new file mode 100644 index 00000000000..037af017ae9 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/FakeChannelCrypter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.base.Preconditions.checkState; + +import io.netty.buffer.ByteBuf; +import java.security.GeneralSecurityException; +import java.util.Collections; +import java.util.List; +import javax.crypto.AEADBadTagException; + +public final class FakeChannelCrypter implements ChannelCrypterNetty { + private static final int TAG_BYTES = 16; + private static final byte TAG_BYTE = (byte) 0xa1; + + private boolean destroyCalled = false; + + public static int getTagBytes() { + return TAG_BYTES; + } + + @Override + public void encrypt(ByteBuf out, List plain) throws GeneralSecurityException { + checkState(!destroyCalled); + for (ByteBuf buf : plain) { + out.writeBytes(buf); + for (int i = 0; i < TAG_BYTES; ++i) { + out.writeByte(TAG_BYTE); + } + } + } + + @Override + public void decrypt(ByteBuf out, ByteBuf tag, List ciphertext) + throws GeneralSecurityException { + checkState(!destroyCalled); + for (ByteBuf buf : ciphertext) { + out.writeBytes(buf); + } + while (tag.isReadable()) { + if (tag.readByte() != TAG_BYTE) { + throw new AEADBadTagException("Tag mismatch!"); + } + } + } + + @Override + public void decrypt(ByteBuf out, ByteBuf ciphertextAndTag) throws GeneralSecurityException { + checkState(!destroyCalled); + ByteBuf ciphertext = ciphertextAndTag.readSlice(ciphertextAndTag.readableBytes() - TAG_BYTES); + decrypt(out, /*tag=*/ ciphertextAndTag, Collections.singletonList(ciphertext)); + } + + @Override + public int getSuffixLength() { + return TAG_BYTES; + } + + @Override + public void destroy() { + destroyCalled = true; + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/FakeTsiHandshaker.java b/alts/src/test/java/io/grpc/alts/internal/FakeTsiHandshaker.java new file mode 100644 index 00000000000..7a6119dc0be --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/FakeTsiHandshaker.java @@ -0,0 +1,239 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Preconditions; +import io.grpc.ChannelLogger; +import io.grpc.alts.internal.TsiPeer.Property; +import io.grpc.internal.TestUtils.NoopChannelLogger; +import io.netty.buffer.ByteBufAllocator; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A fake handshaker compatible with security/transport_security/fake_transport_security.h See + * {@link TsiHandshaker} for documentation. + */ +public class FakeTsiHandshaker implements TsiHandshaker { + private static final Logger logger = Logger.getLogger(FakeTsiHandshaker.class.getName()); + + private static final TsiHandshakerFactory clientHandshakerFactory = + new TsiHandshakerFactory() { + @Override + public TsiHandshaker newHandshaker(String authority, ChannelLogger logger) { + return new FakeTsiHandshaker(true); + } + }; + + private static final TsiHandshakerFactory serverHandshakerFactory = + new TsiHandshakerFactory() { + @Override + public TsiHandshaker newHandshaker(String authority, ChannelLogger logger) { + return new FakeTsiHandshaker(false); + } + }; + + private boolean isClient; + private ByteBuffer sendBuffer = null; + private AltsFraming.Parser frameParser = new AltsFraming.Parser(); + + private State sendState; + private State receiveState; + + enum State { + CLIENT_NONE, + SERVER_NONE, + CLIENT_INIT, + SERVER_INIT, + CLIENT_FINISHED, + SERVER_FINISHED; + + // Returns the next State. In order to advance to sendState=N, receiveState must be N-1. + @SuppressWarnings("EnumOrdinal") + public State next() { + if (ordinal() + 1 < values().length) { + return values()[ordinal() + 1]; + } + throw new UnsupportedOperationException("Can't call next() on last element: " + this); + } + } + + public static TsiHandshakerFactory clientHandshakerFactory() { + return clientHandshakerFactory; + } + + public static TsiHandshakerFactory serverHandshakerFactory() { + return serverHandshakerFactory; + } + + public static TsiHandshaker newFakeHandshakerClient() { + NoopChannelLogger channelLogger = new NoopChannelLogger(); + return clientHandshakerFactory.newHandshaker(null, channelLogger); + } + + public static TsiHandshaker newFakeHandshakerServer() { + NoopChannelLogger channelLogger = new NoopChannelLogger(); + return serverHandshakerFactory.newHandshaker(null, channelLogger); + } + + protected FakeTsiHandshaker(boolean isClient) { + this.isClient = isClient; + if (isClient) { + sendState = State.CLIENT_NONE; + receiveState = State.SERVER_NONE; + } else { + sendState = State.SERVER_NONE; + receiveState = State.CLIENT_NONE; + } + } + + private State getNextState(State state) { + switch (state) { + case CLIENT_NONE: + return State.CLIENT_INIT; + case SERVER_NONE: + return State.SERVER_INIT; + case CLIENT_INIT: + return State.CLIENT_FINISHED; + case SERVER_INIT: + return State.SERVER_FINISHED; + default: + return null; + } + } + + private String getNextMessage() { + State result = getNextState(sendState); + return result == null ? "BAD STATE" : result.toString(); + } + + private String getExpectedMessage() { + State result = getNextState(receiveState); + return result == null ? "BAD STATE" : result.toString(); + } + + private void incrementSendState() { + sendState = getNextState(sendState); + } + + private void incrementReceiveState() { + receiveState = getNextState(receiveState); + } + + @Override + public void getBytesToSendToPeer(ByteBuffer bytes) throws GeneralSecurityException { + Preconditions.checkNotNull(bytes); + + // If we're done, return nothing. + if (sendState == State.CLIENT_FINISHED || sendState == State.SERVER_FINISHED) { + return; + } + + // Prepare the next message, if needed. + if (sendBuffer == null) { + if (sendState.next() != receiveState) { + // We're still waiting for bytes from the peer, so bail. + return; + } + ByteBuffer payload = ByteBuffer.wrap(getNextMessage().getBytes(UTF_8)); + sendBuffer = AltsFraming.toFrame(payload, payload.remaining()); + logger.log(Level.FINE, "Buffered message: {0}", getNextMessage()); + } + while (bytes.hasRemaining() && sendBuffer.hasRemaining()) { + bytes.put(sendBuffer.get()); + } + if (!sendBuffer.hasRemaining()) { + // Get ready to send the next message. + sendBuffer = null; + incrementSendState(); + } + } + + @Override + public boolean processBytesFromPeer(ByteBuffer bytes) throws GeneralSecurityException { + Preconditions.checkNotNull(bytes); + + frameParser.readBytes(bytes); + if (frameParser.isComplete()) { + ByteBuffer messageBytes = frameParser.getRawFrame(); + int offset = AltsFraming.getFramingOverhead(); + int length = messageBytes.limit() - offset; + @SuppressWarnings("ByteBufferBackingArray") // ByteBuffer is created using allocate() + String message = new String(messageBytes.array(), offset, length, UTF_8); + logger.log(Level.FINE, "Read message: {0}", message); + + if (!message.equals(getExpectedMessage())) { + throw new IllegalArgumentException( + "Bad handshake message. Got " + + message + + " (length = " + + message.length() + + ") expected " + + getExpectedMessage() + + " (length = " + + getExpectedMessage().length() + + ")"); + } + incrementReceiveState(); + return true; + } + return false; + } + + @Override + public boolean isInProgress() { + boolean finishedReceiving = + receiveState == State.CLIENT_FINISHED || receiveState == State.SERVER_FINISHED; + boolean finishedSending = + sendState == State.CLIENT_FINISHED || sendState == State.SERVER_FINISHED; + return !finishedSending || !finishedReceiving; + } + + @Override + public TsiPeer extractPeer() { + return new TsiPeer(Collections.>emptyList()); + } + + @Override + public Object extractPeerObject() { + return AltsInternalContext.getDefaultInstance(); + } + + @Override + public TsiFrameProtector createFrameProtector(int maxFrameSize, ByteBufAllocator alloc) { + Preconditions.checkState(!isInProgress(), "Handshake is not complete."); + + // We use an all-zero key, since this is the fake handshaker. + byte[] key = new byte[AltsChannelCrypter.getKeyLength()]; + return new AltsTsiFrameProtector(maxFrameSize, new AltsChannelCrypter(key, isClient), alloc); + } + + @Override + public TsiFrameProtector createFrameProtector(ByteBufAllocator alloc) { + return createFrameProtector(AltsTsiFrameProtector.getMinFrameSize(), alloc); + } + + @Override + public void close() { + // No-op + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/FakeTsiTest.java b/alts/src/test/java/io/grpc/alts/internal/FakeTsiTest.java new file mode 100644 index 00000000000..f908160f958 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/FakeTsiTest.java @@ -0,0 +1,214 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import com.google.common.testing.GcFinalization; +import io.grpc.alts.internal.ByteBufTestUtils.RegisterRef; +import io.grpc.alts.internal.TsiTest.Handshakers; +import io.netty.buffer.ByteBuf; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetector.Level; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link TsiHandshaker}. */ +@RunWith(JUnit4.class) +public class FakeTsiTest { + + private static final int OVERHEAD = + FakeChannelCrypter.getTagBytes() + AltsTsiFrameProtector.getHeaderBytes(); + + private final List references = new ArrayList<>(); + private final RegisterRef ref = + new RegisterRef() { + @Override + public ByteBuf register(ByteBuf buf) { + if (buf != null) { + references.add(buf); + } + return buf; + } + }; + + private static Handshakers newHandshakers() { + TsiHandshaker clientHandshaker = FakeTsiHandshaker.newFakeHandshakerClient(); + TsiHandshaker serverHandshaker = FakeTsiHandshaker.newFakeHandshakerServer(); + return new Handshakers(clientHandshaker, serverHandshaker); + } + + @Before + public void setUp() { + ResourceLeakDetector.setLevel(Level.PARANOID); + } + + @After + public void tearDown() { + for (ReferenceCounted reference : references) { + reference.release(); + } + references.clear(); + // Increase our chances to detect ByteBuf leaks. + GcFinalization.awaitFullGc(); + } + + @Test + public void handshakeStateOrderTest() { + try { + Handshakers handshakers = newHandshakers(); + TsiHandshaker clientHandshaker = handshakers.getClient(); + TsiHandshaker serverHandshaker = handshakers.getServer(); + + byte[] transportBufferBytes = new byte[TsiTest.getDefaultTransportBufferSize()]; + ByteBuffer transportBuffer = ByteBuffer.wrap(transportBufferBytes); + ((Buffer) transportBuffer).limit(0); // Start off with an empty buffer + + ((Buffer) transportBuffer).clear(); + clientHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertEquals( + FakeTsiHandshaker.State.CLIENT_INIT.toString().trim(), + new String(transportBufferBytes, 4, transportBuffer.remaining(), UTF_8).trim()); + + serverHandshaker.processBytesFromPeer(transportBuffer); + assertFalse(transportBuffer.hasRemaining()); + + // client shouldn't offer any more bytes + ((Buffer) transportBuffer).clear(); + clientHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertFalse(transportBuffer.hasRemaining()); + + ((Buffer) transportBuffer).clear(); + serverHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertEquals( + FakeTsiHandshaker.State.SERVER_INIT.toString().trim(), + new String(transportBufferBytes, 4, transportBuffer.remaining(), UTF_8).trim()); + + clientHandshaker.processBytesFromPeer(transportBuffer); + assertFalse(transportBuffer.hasRemaining()); + + // server shouldn't offer any more bytes + ((Buffer) transportBuffer).clear(); + serverHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertFalse(transportBuffer.hasRemaining()); + + ((Buffer) transportBuffer).clear(); + clientHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertEquals( + FakeTsiHandshaker.State.CLIENT_FINISHED.toString().trim(), + new String(transportBufferBytes, 4, transportBuffer.remaining(), UTF_8).trim()); + + serverHandshaker.processBytesFromPeer(transportBuffer); + assertFalse(transportBuffer.hasRemaining()); + + // client shouldn't offer any more bytes + ((Buffer) transportBuffer).clear(); + clientHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertFalse(transportBuffer.hasRemaining()); + + ((Buffer) transportBuffer).clear(); + serverHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertEquals( + FakeTsiHandshaker.State.SERVER_FINISHED.toString().trim(), + new String(transportBufferBytes, 4, transportBuffer.remaining(), UTF_8).trim()); + + clientHandshaker.processBytesFromPeer(transportBuffer); + assertFalse(transportBuffer.hasRemaining()); + + // server shouldn't offer any more bytes + ((Buffer) transportBuffer).clear(); + serverHandshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + assertFalse(transportBuffer.hasRemaining()); + } catch (GeneralSecurityException e) { + throw new AssertionError(e); + } + } + + @Test + public void handshake() throws GeneralSecurityException { + TsiTest.handshakeTest(newHandshakers()); + } + + @Test + public void handshakeSmallBuffer() throws GeneralSecurityException { + TsiTest.handshakeSmallBufferTest(newHandshakers()); + } + + @Test + public void pingPong() throws GeneralSecurityException { + TsiTest.pingPongTest(newHandshakers(), ref); + } + + @Test + public void pingPongExactFrameSize() throws GeneralSecurityException { + TsiTest.pingPongExactFrameSizeTest(newHandshakers(), ref); + } + + @Test + public void pingPongSmallBuffer() throws GeneralSecurityException { + TsiTest.pingPongSmallBufferTest(newHandshakers(), ref); + } + + @Test + public void pingPongSmallFrame() throws GeneralSecurityException { + TsiTest.pingPongSmallFrameTest(OVERHEAD, newHandshakers(), ref); + } + + @Test + public void pingPongSmallFrameSmallBuffer() throws GeneralSecurityException { + TsiTest.pingPongSmallFrameSmallBufferTest(OVERHEAD, newHandshakers(), ref); + } + + @Test + public void corruptedCounter() throws GeneralSecurityException { + TsiTest.corruptedCounterTest(newHandshakers(), ref); + } + + @Test + public void corruptedCiphertext() throws GeneralSecurityException { + TsiTest.corruptedCiphertextTest(newHandshakers(), ref); + } + + @Test + public void corruptedTag() throws GeneralSecurityException { + TsiTest.corruptedTagTest(newHandshakers(), ref); + } + + @Test + public void reflectedCiphertext() throws GeneralSecurityException { + TsiTest.reflectedCiphertextTest(newHandshakers(), ref); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java new file mode 100644 index 00000000000..14c19e554ae --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import io.grpc.Attributes; +import io.grpc.Channel; +import io.grpc.ChannelLogger; +import io.grpc.ChannelLogger.ChannelLogLevel; +import io.grpc.ManagedChannel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.internal.ObjectPool; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.InternalProtocolNegotiationEvent; +import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.ssl.SslContext; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(Enclosed.class) +public final class GoogleDefaultProtocolNegotiatorTest { + + @RunWith(JUnit4.class) + public abstract static class HandlerSelectionTest { + private ProtocolNegotiator googleProtocolNegotiator; + private Attributes.Key originalClusterNameAttrKey; + private final ObjectPool handshakerChannelPool = new ObjectPool() { + + @Override + public Channel getObject() { + return InProcessChannelBuilder.forName("test").build(); + } + + @Override + public Channel returnObject(Object object) { + ((ManagedChannel) object).shutdownNow(); + return null; + } + }; + + @Before + public void setUp() throws Exception { + SslContext sslContext = GrpcSslContexts.forClient().build(); + originalClusterNameAttrKey = + AltsProtocolNegotiator.GoogleDefaultProtocolNegotiatorFactory.clusterNameAttrKey; + AltsProtocolNegotiator.GoogleDefaultProtocolNegotiatorFactory.clusterNameAttrKey = + getClusterNameAttrKey(); + googleProtocolNegotiator = new AltsProtocolNegotiator.GoogleDefaultProtocolNegotiatorFactory( + ImmutableList.of(), + handshakerChannelPool, + sslContext) + .newNegotiator(); + } + + @After + public void tearDown() { + googleProtocolNegotiator.close(); + AltsProtocolNegotiator.GoogleDefaultProtocolNegotiatorFactory.clusterNameAttrKey = + originalClusterNameAttrKey; + } + + @Nullable + abstract Attributes.Key getClusterNameAttrKey(); + + @Test + public void tlsHandler_emptyAttributes() { + subtest_tlsHandler(Attributes.EMPTY); + } + + void subtest_altsHandler(Attributes eagAttributes) { + GrpcHttp2ConnectionHandler mockHandler = mock(GrpcHttp2ConnectionHandler.class); + when(mockHandler.getEagAttributes()).thenReturn(eagAttributes); + ChannelLogger logger = mock(ChannelLogger.class); + doNothing().when(logger).log(any(ChannelLogLevel.class), anyString()); + when(mockHandler.getNegotiationLogger()).thenReturn(logger); + + final AtomicReference failure = new AtomicReference<>(); + ChannelHandler exceptionCaught = new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + failure.set(cause); + super.exceptionCaught(ctx, cause); + } + }; + ChannelHandler h = googleProtocolNegotiator.newHandler(mockHandler); + EmbeddedChannel chan = new EmbeddedChannel(exceptionCaught); + // Add the negotiator handler last, but to the front. Putting this in ctor above would make + // it throw early. + chan.pipeline().addFirst(h); + chan.pipeline().fireUserEventTriggered(InternalProtocolNegotiationEvent.getDefault()); + + // Check that the message complained about the ALTS code, rather than SSL. ALTS throws on + // being added, so it's hard to catch it at the right time to make this assertion. + assertThat(failure.get()).hasMessageThat().contains("TsiHandshakeHandler"); + } + + void subtest_tlsHandler(Attributes eagAttributes) { + GrpcHttp2ConnectionHandler mockHandler = mock(GrpcHttp2ConnectionHandler.class); + when(mockHandler.getEagAttributes()).thenReturn(eagAttributes); + when(mockHandler.getAuthority()).thenReturn("authority"); + ChannelLogger logger = mock(ChannelLogger.class); + doNothing().when(logger).log(any(ChannelLogLevel.class), anyString()); + when(mockHandler.getNegotiationLogger()).thenReturn(logger); + + ChannelHandler h = googleProtocolNegotiator.newHandler(mockHandler); + EmbeddedChannel chan = new EmbeddedChannel(h); + chan.pipeline().fireUserEventTriggered(InternalProtocolNegotiationEvent.getDefault()); + + assertThat(chan.pipeline().first().getClass().getSimpleName()).isEqualTo("SslHandler"); + } + } + + @RunWith(JUnit4.class) + public static class WithoutXdsInClasspath extends HandlerSelectionTest { + + @Nullable + @Override + Attributes.Key getClusterNameAttrKey() { + return null; + } + } + + @RunWith(JUnit4.class) + public static class WithXdsInClasspath extends HandlerSelectionTest { + // Same as io.grpc.xds.InternalXdsAttributes.ATTR_CLUSTER_NAME + private static final Attributes.Key XDS_CLUSTER_NAME_ATTR_KEY = + Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.clusterName"); + + @Nullable + @Override + Attributes.Key getClusterNameAttrKey() { + return XDS_CLUSTER_NAME_ATTR_KEY; + } + + @Test + public void altsHandler_xdsCluster() { + Attributes attrs = + Attributes.newBuilder().set(XDS_CLUSTER_NAME_ATTR_KEY, "api.googleapis.com").build(); + subtest_altsHandler(attrs); + } + + @Test + public void tlsHandler_googleCfe() { + Attributes attrs = Attributes.newBuilder().set( + XDS_CLUSTER_NAME_ATTR_KEY, "google_cfe_api.googleapis.com").build(); + subtest_tlsHandler(attrs); + } + + @Test + public void altsHandler_googleCfe_federation() { + Attributes attrs = Attributes.newBuilder().set( + XDS_CLUSTER_NAME_ATTR_KEY, "xdstp1://").build(); + subtest_altsHandler(attrs); + } + + @Test + public void tlsHanlder_googleCfe() { + Attributes attrs = Attributes.newBuilder().set( + XDS_CLUSTER_NAME_ATTR_KEY, + "xdstp://traffic-director-c2p.xds.googleapis.com/" + + "envoy.config.cluster.v3.Cluster/google_cfe_example/apis") + .build(); + subtest_tlsHandler(attrs); + } + + @Test + public void altsHanlder_nonGoogleCfe_authorityNotMatch() { + Attributes attrs = Attributes.newBuilder().set( + XDS_CLUSTER_NAME_ATTR_KEY, + "//example.com/envoy.config.cluster.v3.Cluster/google_cfe_") + .build(); + subtest_altsHandler(attrs); + } + + @Test + public void altsHanlder_nonGoogleCfe_pathNotMatch() { + Attributes attrs = Attributes.newBuilder().set( + XDS_CLUSTER_NAME_ATTR_KEY, + "//traffic-director-c2p.xds.googleapis.com/envoy.config.cluster.v3.Cluster/google_gfe") + .build(); + subtest_altsHandler(attrs); + } + + @Test + public void altsHandler_googleCfe_invalidUri() { + Attributes attrs = Attributes.newBuilder().set( + XDS_CLUSTER_NAME_ATTR_KEY, "//").build(); + subtest_altsHandler(attrs); + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/MockAltsHandshakerResp.java b/alts/src/test/java/io/grpc/alts/internal/MockAltsHandshakerResp.java new file mode 100644 index 00000000000..a21082be41f --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/MockAltsHandshakerResp.java @@ -0,0 +1,114 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.SecureRandom; +import java.util.Random; + +/** A class for mocking ALTS Handshaker Responses. */ +class MockAltsHandshakerResp { + private static final String TEST_ERROR_DETAILS = "handshake error"; + private static final String TEST_APPLICATION_PROTOCOL = "grpc"; + private static final String TEST_RECORD_PROTOCOL = "ALTSRP_GCM_AES128"; + private static final String TEST_OUT_FRAME = "output frame"; + private static final String TEST_LOCAL_ACCOUNT = "local@developer.gserviceaccount.com"; + private static final String TEST_PEER_ACCOUNT = "peer@developer.gserviceaccount.com"; + private static final byte[] TEST_KEY_DATA = initializeTestKeyData(); + private static final int FRAME_HEADER_SIZE = 4; + + static String getTestErrorDetails() { + return TEST_ERROR_DETAILS; + } + + static String getTestPeerAccount() { + return TEST_PEER_ACCOUNT; + } + + private static byte[] initializeTestKeyData() { + Random random = new SecureRandom(); + byte[] randombytes = new byte[AltsChannelCrypter.getKeyLength()]; + random.nextBytes(randombytes); + return randombytes; + } + + static byte[] getTestKeyData() { + return TEST_KEY_DATA; + } + + /** Returns a mock output frame. */ + static ByteString getOutFrame() { + int frameSize = TEST_OUT_FRAME.length(); + ByteBuffer buffer = ByteBuffer.allocate(FRAME_HEADER_SIZE + frameSize); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(frameSize); + buffer.put(TEST_OUT_FRAME.getBytes(UTF_8)); + ((Buffer) buffer).flip(); + return ByteString.copyFrom(buffer); + } + + /** Returns a mock error handshaker response. */ + static HandshakerResp getErrorResponse() { + HandshakerResp.Builder resp = HandshakerResp.newBuilder(); + resp.setStatus( + HandshakerStatus.newBuilder() + .setCode(Status.Code.UNKNOWN.value()) + .setDetails(TEST_ERROR_DETAILS) + .build()); + return resp.build(); + } + + /** Returns a mock normal handshaker response. */ + static HandshakerResp getOkResponse(int bytesConsumed) { + HandshakerResp.Builder resp = HandshakerResp.newBuilder(); + resp.setOutFrames(getOutFrame()); + resp.setBytesConsumed(bytesConsumed); + resp.setStatus(HandshakerStatus.newBuilder().setCode(Status.Code.OK.value()).build()); + return resp.build(); + } + + /** Returns a mock normal handshaker response. */ + static HandshakerResp getEmptyOutFrameResponse(int bytesConsumed) { + HandshakerResp.Builder resp = HandshakerResp.newBuilder(); + resp.setBytesConsumed(bytesConsumed); + resp.setStatus(HandshakerStatus.newBuilder().setCode(Status.Code.OK.value()).build()); + return resp.build(); + } + + /** Returns a mock final handshaker response with handshake result. */ + static HandshakerResp getFinishedResponse(int bytesConsumed) { + HandshakerResp.Builder resp = HandshakerResp.newBuilder(); + HandshakerResult.Builder result = + HandshakerResult.newBuilder() + .setApplicationProtocol(TEST_APPLICATION_PROTOCOL) + .setRecordProtocol(TEST_RECORD_PROTOCOL) + .setPeerIdentity(Identity.newBuilder().setServiceAccount(TEST_PEER_ACCOUNT).build()) + .setLocalIdentity(Identity.newBuilder().setServiceAccount(TEST_LOCAL_ACCOUNT).build()) + .setKeyData(ByteString.copyFrom(TEST_KEY_DATA)); + resp.setOutFrames(getOutFrame()); + resp.setBytesConsumed(bytesConsumed); + resp.setStatus(HandshakerStatus.newBuilder().setCode(Status.Code.OK.value()).build()); + resp.setResult(result.build()); + return resp.build(); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/NettyTsiHandshakerTest.java b/alts/src/test/java/io/grpc/alts/internal/NettyTsiHandshakerTest.java new file mode 100644 index 00000000000..d71ff16f352 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/NettyTsiHandshakerTest.java @@ -0,0 +1,204 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.util.ReferenceCounted; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class NettyTsiHandshakerTest { + private final UnpooledByteBufAllocator alloc = UnpooledByteBufAllocator.DEFAULT; + private final List references = new ArrayList<>(); + + private final NettyTsiHandshaker clientHandshaker = + new NettyTsiHandshaker(FakeTsiHandshaker.newFakeHandshakerClient()); + private final NettyTsiHandshaker serverHandshaker = + new NettyTsiHandshaker(FakeTsiHandshaker.newFakeHandshakerServer()); + + @After + public void teardown() { + for (ReferenceCounted reference : references) { + reference.release(reference.refCnt()); + } + } + + @Test + public void failsOnNullHandshaker() { + try { + new NettyTsiHandshaker(null); + fail("Exception expected"); + } catch (NullPointerException ex) { + // Do nothing. + } + } + + @Test + public void processPeerHandshakeShouldAcceptPartialFrames() throws GeneralSecurityException { + for (int i = 0; i < 1024; i++) { + ByteBuf clientData = ref(alloc.buffer(1)); + clientHandshaker.getBytesToSendToPeer(clientData); + if (clientData.isReadable()) { + if (serverHandshaker.processBytesFromPeer(clientData)) { + // Done. + return; + } + } + } + fail("Failed to process the handshake frame."); + } + + @Test + public void handshakeShouldSucceed() throws GeneralSecurityException { + doHandshake(); + } + + @Test + public void isInProgress() throws GeneralSecurityException { + assertTrue(clientHandshaker.isInProgress()); + assertTrue(serverHandshaker.isInProgress()); + + doHandshake(); + + assertFalse(clientHandshaker.isInProgress()); + assertFalse(serverHandshaker.isInProgress()); + } + + @Test + public void extractPeer_notNull() throws GeneralSecurityException { + doHandshake(); + + assertNotNull(serverHandshaker.extractPeer()); + assertNotNull(clientHandshaker.extractPeer()); + } + + @Test + public void extractPeer_failsBeforeHandshake() throws GeneralSecurityException { + try { + clientHandshaker.extractPeer(); + fail("Exception expected"); + } catch (IllegalStateException ex) { + // Do nothing. + } + } + + @Test + public void extractPeerObject_notNull() throws GeneralSecurityException { + doHandshake(); + + assertNotNull(serverHandshaker.extractPeerObject()); + assertNotNull(clientHandshaker.extractPeerObject()); + } + + @Test + public void extractPeerObject_failsBeforeHandshake() throws GeneralSecurityException { + try { + clientHandshaker.extractPeerObject(); + fail("Exception expected"); + } catch (IllegalStateException ex) { + // Do nothing. + } + } + + /** + * NettyTsiHandshaker just converts {@link ByteBuffer} to {@link ByteBuf}, so check that the other + * methods are otherwise the same. + */ + @Test + public void handshakerMethodsMatch() { + List expectedMethods = new ArrayList<>(); + for (Method m : TsiHandshaker.class.getDeclaredMethods()) { + expectedMethods.add(m.getName()); + } + + List actualMethods = new ArrayList<>(); + for (Method m : NettyTsiHandshaker.class.getDeclaredMethods()) { + actualMethods.add(m.getName()); + } + + assertThat(actualMethods).containsAtLeastElementsIn(expectedMethods); + } + + static void doHandshake( + NettyTsiHandshaker clientHandshaker, + NettyTsiHandshaker serverHandshaker, + ByteBufAllocator alloc, + Function ref) + throws GeneralSecurityException { + // Get the server response handshake frames. + for (int i = 0; i < 10; i++) { + if (!(clientHandshaker.isInProgress() || serverHandshaker.isInProgress())) { + return; + } + + ByteBuf clientData = ref.apply(alloc.buffer()); + clientHandshaker.getBytesToSendToPeer(clientData); + if (clientData.isReadable()) { + serverHandshaker.processBytesFromPeer(clientData); + } + + ByteBuf serverData = ref.apply(alloc.buffer()); + serverHandshaker.getBytesToSendToPeer(serverData); + if (serverData.isReadable()) { + clientHandshaker.processBytesFromPeer(serverData); + } + } + + throw new AssertionError("Failed to complete the handshake."); + } + + private void doHandshake() throws GeneralSecurityException { + doHandshake( + clientHandshaker, + serverHandshaker, + alloc, + new Function() { + @Override + public ByteBuf apply(ByteBuf buf) { + return ref(buf); + } + }); + } + + private ByteBuf ref(ByteBuf buf) { + if (buf != null) { + references.add(buf); + } + return buf; + } + + /** A mirror of java.util.function.Function without the Java 8 dependency. */ + private interface Function { + R apply(T t); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/RpcProtocolVersionsUtilTest.java b/alts/src/test/java/io/grpc/alts/internal/RpcProtocolVersionsUtilTest.java new file mode 100644 index 00000000000..07e0358e707 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/RpcProtocolVersionsUtilTest.java @@ -0,0 +1,247 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import io.grpc.alts.internal.RpcProtocolVersions.Version; +import io.grpc.alts.internal.RpcProtocolVersionsUtil.RpcVersionsCheckResult; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link RpcProtocolVersionsUtil}. */ +@RunWith(JUnit4.class) +public final class RpcProtocolVersionsUtilTest { + + @Test + public void compareVersions() throws Exception { + assertTrue( + RpcProtocolVersionsUtil.isGreaterThanOrEqualTo( + Version.newBuilder().setMajor(3).setMinor(2).build(), + Version.newBuilder().setMajor(2).setMinor(1).build())); + assertTrue( + RpcProtocolVersionsUtil.isGreaterThanOrEqualTo( + Version.newBuilder().setMajor(3).setMinor(2).build(), + Version.newBuilder().setMajor(2).setMinor(1).build())); + assertTrue( + RpcProtocolVersionsUtil.isGreaterThanOrEqualTo( + Version.newBuilder().setMajor(3).setMinor(2).build(), + Version.newBuilder().setMajor(3).setMinor(2).build())); + assertFalse( + RpcProtocolVersionsUtil.isGreaterThanOrEqualTo( + Version.newBuilder().setMajor(2).setMinor(3).build(), + Version.newBuilder().setMajor(3).setMinor(2).build())); + assertFalse( + RpcProtocolVersionsUtil.isGreaterThanOrEqualTo( + Version.newBuilder().setMajor(3).setMinor(1).build(), + Version.newBuilder().setMajor(3).setMinor(2).build())); + } + + @Test + public void checkRpcVersions() throws Exception { + // local.max > peer.max and local.min > peer.min + RpcVersionsCheckResult checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(3).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // local.max > peer.max and local.min < peer.min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(3).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // local.max > peer.max and local.min = peer.min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(3).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // local.max < peer.max and local.min > peer.min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(3).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // local.max = peer.max and local.min > peer.min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // local.max < peer.max and local.min < peer.min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(3).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // local.max < peer.max and local.min = peer.min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(3).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // local.max = peer.max and local.min < peer.min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // all equal + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build()); + assertTrue(checkResult.getResult()); + assertEquals( + Version.newBuilder().setMajor(2).setMinor(1).build(), + checkResult.getHighestCommonVersion()); + // max is smaller than min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(1).setMinor(2).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build()); + assertFalse(checkResult.getResult()); + assertEquals(null, checkResult.getHighestCommonVersion()); + // no overlap, local > peer + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(6).setMinor(5).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(4).setMinor(3).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(0).setMinor(0).build()) + .build()); + assertFalse(checkResult.getResult()); + assertEquals(null, checkResult.getHighestCommonVersion()); + // no overlap, local < peer + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(1).setMinor(0).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(6).setMinor(5).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(4).setMinor(3).build()) + .build()); + assertFalse(checkResult.getResult()); + assertEquals(null, checkResult.getHighestCommonVersion()); + // no overlap, max < min + checkResult = + RpcProtocolVersionsUtil.checkRpcProtocolVersions( + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(4).setMinor(3).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(6).setMinor(5).build()) + .build(), + RpcProtocolVersions.newBuilder() + .setMaxRpcVersion(Version.newBuilder().setMajor(1).setMinor(0).build()) + .setMinRpcVersion(Version.newBuilder().setMajor(2).setMinor(1).build()) + .build()); + assertFalse(checkResult.getResult()); + assertEquals(null, checkResult.getHighestCommonVersion()); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/TsiFrameHandlerTest.java b/alts/src/test/java/io/grpc/alts/internal/TsiFrameHandlerTest.java new file mode 100644 index 00000000000..f9ed0fa4f97 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/TsiFrameHandlerTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.CharsetUtil; +import java.security.GeneralSecurityException; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link TsiFrameHandler}. */ +@RunWith(JUnit4.class) +public class TsiFrameHandlerTest { + + @Rule + public final TestRule globalTimeout = new DisableOnDebug(Timeout.seconds(5)); + + private final TsiFrameHandler tsiFrameHandler = new TsiFrameHandler(new IdentityFrameProtector()); + private final EmbeddedChannel channel = new EmbeddedChannel(tsiFrameHandler); + + @Test + public void writeAndFlush_handshakeSucceed() throws InterruptedException { + ByteBuf msg = Unpooled.copiedBuffer("message after handshake finished", CharsetUtil.UTF_8); + + channel.writeAndFlush(msg); + Object actual = channel.readOutbound(); + + assertThat(actual).isEqualTo(msg); + channel.close().sync(); + channel.checkException(); + } + + @Test + public void writeAndFlush_shouldBeIgnoredAfterClose() throws InterruptedException { + channel.close().sync(); + ByteBuf msg = Unpooled.copiedBuffer("message after closed", CharsetUtil.UTF_8); + + channel.writeAndFlush(msg); + + assertThat(channel.outboundMessages()).isEmpty(); + try { + channel.checkException(); + } catch (Exception e) { + throw new AssertionError( + "Any attempt after close should be ignored without out exception", e); + } + } + + @Test + public void close_shouldFlushRemainingMessage() throws InterruptedException { + ByteBuf msg = Unpooled.copiedBuffer("message after handshake failed", CharsetUtil.UTF_8); + channel.write(msg); + + assertThat(channel.outboundMessages()).isEmpty(); + + channel.close().sync(); + Object actual = channel.readOutbound(); + + assertWithMessage("pending write should be flushed on close").that(actual).isEqualTo(msg); + channel.checkException(); + } + + @Test + public void flushAfterCloseShouldWork() throws InterruptedException { + ByteBuf msg = Unpooled.copiedBuffer("message after handshake failed", CharsetUtil.UTF_8); + channel.write(msg); + + channel.pipeline().addFirst(new ChannelOutboundHandlerAdapter() { + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + // We have to call flush while doing a close, since close() tears down the pipeline + // immediately after. + channel.flush(); + super.close(ctx, promise); + } + }); + + assertThat(channel.outboundMessages()).isEmpty(); + + channel.close().sync(); + Object actual = channel.readOutbound(); + + assertWithMessage("pending write should be flushed on close").that(actual).isEqualTo(msg); + channel.checkException(); + } + + private static final class IdentityFrameProtector implements TsiFrameProtector { + + @Override + public void protectFlush(List unprotectedBufs, Consumer ctxWrite, + ByteBufAllocator alloc) throws GeneralSecurityException { + for (ByteBuf unprotectedBuf : unprotectedBufs) { + ctxWrite.accept(unprotectedBuf); + } + } + + @Override + public void unprotect(ByteBuf in, List out, ByteBufAllocator alloc) + throws GeneralSecurityException { + out.add(in.toString(CharsetUtil.UTF_8)); + } + + @Override + public void destroy() {} + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/TsiTest.java b/alts/src/test/java/io/grpc/alts/internal/TsiTest.java new file mode 100644 index 00000000000..0241182f2db --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/TsiTest.java @@ -0,0 +1,406 @@ +/* + * Copyright 2018 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts.internal; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.alts.internal.ByteBufTestUtils.getDirectBuffer; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; + +import io.grpc.alts.internal.ByteBufTestUtils.RegisterRef; +import io.grpc.alts.internal.TsiFrameProtector.Consumer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.crypto.AEADBadTagException; + +/** Utility class that provides tests for implementations of {@link TsiHandshaker}. */ +public final class TsiTest { + private static final String DECRYPTION_FAILURE_RE = "Tag mismatch|BAD_DECRYPT"; + + private TsiTest() {} + + /** A {@code TsiHandshaker} pair for running tests. */ + public static class Handshakers { + private final TsiHandshaker client; + private final TsiHandshaker server; + + public Handshakers(TsiHandshaker client, TsiHandshaker server) { + this.client = client; + this.server = server; + } + + public TsiHandshaker getClient() { + return client; + } + + public TsiHandshaker getServer() { + return server; + } + } + + private static final int DEFAULT_TRANSPORT_BUFFER_SIZE = 2048; + + private static final UnpooledByteBufAllocator alloc = UnpooledByteBufAllocator.DEFAULT; + + private static final String EXAMPLE_MESSAGE1 = "hello world"; + private static final String EXAMPLE_MESSAGE2 = "oysteroystersoysterseateateat"; + + private static final int EXAMPLE_MESSAGE1_LEN = EXAMPLE_MESSAGE1.getBytes(UTF_8).length; + private static final int EXAMPLE_MESSAGE2_LEN = EXAMPLE_MESSAGE2.getBytes(UTF_8).length; + + static int getDefaultTransportBufferSize() { + return DEFAULT_TRANSPORT_BUFFER_SIZE; + } + + /** + * Performs a handshake between the client handshaker and server handshaker using a transport of + * length transportBufferSize. + */ + static void performHandshake(int transportBufferSize, Handshakers handshakers) + throws GeneralSecurityException { + TsiHandshaker clientHandshaker = handshakers.getClient(); + TsiHandshaker serverHandshaker = handshakers.getServer(); + + byte[] transportBufferBytes = new byte[transportBufferSize]; + ByteBuffer transportBuffer = ByteBuffer.wrap(transportBufferBytes); + ((Buffer) transportBuffer).limit(0); // Start off with an empty buffer + + while (clientHandshaker.isInProgress() || serverHandshaker.isInProgress()) { + for (TsiHandshaker handshaker : new TsiHandshaker[] {clientHandshaker, serverHandshaker}) { + if (handshaker.isInProgress()) { + // Process any bytes on the wire. + if (transportBuffer.hasRemaining()) { + handshaker.processBytesFromPeer(transportBuffer); + } + // Put new bytes on the wire, if needed. + if (handshaker.isInProgress()) { + ((Buffer) transportBuffer).clear(); + handshaker.getBytesToSendToPeer(transportBuffer); + ((Buffer) transportBuffer).flip(); + } + } + } + } + clientHandshaker.extractPeer(); + serverHandshaker.extractPeer(); + } + + public static void handshakeTest(Handshakers handshakers) throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + } + + public static void handshakeSmallBufferTest(Handshakers handshakers) + throws GeneralSecurityException { + performHandshake(9, handshakers); + } + + /** Sends a message between the sender and receiver. */ + private static void sendMessage( + TsiFrameProtector sender, + TsiFrameProtector receiver, + int recvFragmentSize, + String message, + RegisterRef ref) + throws GeneralSecurityException { + + ByteBuf plaintextBuffer = Unpooled.wrappedBuffer(message.getBytes(UTF_8)); + final List protectOut = new ArrayList<>(); + List unprotectOut = new ArrayList<>(); + + sender.protectFlush( + Collections.singletonList(plaintextBuffer), + new Consumer() { + @Override + public void accept(ByteBuf buf) { + protectOut.add(buf); + } + }, + alloc); + assertThat(protectOut.size()).isEqualTo(1); + + ByteBuf protect = ref.register(protectOut.get(0)); + while (protect.isReadable()) { + ByteBuf buf = protect; + if (recvFragmentSize > 0) { + int size = Math.min(protect.readableBytes(), recvFragmentSize); + buf = protect.readSlice(size); + } + receiver.unprotect(buf, unprotectOut, alloc); + } + ByteBuf plaintextRecvd = getDirectBuffer(message.getBytes(UTF_8).length, ref); + for (Object unprotect : unprotectOut) { + ByteBuf unprotectBuf = ref.register((ByteBuf) unprotect); + plaintextRecvd.writeBytes(unprotectBuf); + } + assertThat(plaintextRecvd).isEqualTo(Unpooled.wrappedBuffer(message.getBytes(UTF_8))); + } + + /** Ping pong test. */ + public static void pingPongTest(Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + TsiFrameProtector clientProtector = handshakers.getClient().createFrameProtector(alloc); + TsiFrameProtector serverProtector = handshakers.getServer().createFrameProtector(alloc); + + sendMessage(clientProtector, serverProtector, -1, EXAMPLE_MESSAGE1, ref); + sendMessage(serverProtector, clientProtector, -1, EXAMPLE_MESSAGE2, ref); + + clientProtector.destroy(); + serverProtector.destroy(); + } + + /** Ping pong test with exact frame size. */ + public static void pingPongExactFrameSizeTest(Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + int frameSize = + EXAMPLE_MESSAGE1.getBytes(UTF_8).length + + AltsTsiFrameProtector.getHeaderBytes() + + FakeChannelCrypter.getTagBytes(); + + TsiFrameProtector clientProtector = + handshakers.getClient().createFrameProtector(frameSize, alloc); + TsiFrameProtector serverProtector = + handshakers.getServer().createFrameProtector(frameSize, alloc); + + sendMessage(clientProtector, serverProtector, -1, EXAMPLE_MESSAGE1, ref); + sendMessage(serverProtector, clientProtector, -1, EXAMPLE_MESSAGE1, ref); + + clientProtector.destroy(); + serverProtector.destroy(); + } + + /** Ping pong test with small buffer size. */ + public static void pingPongSmallBufferTest(Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + TsiFrameProtector clientProtector = handshakers.getClient().createFrameProtector(alloc); + TsiFrameProtector serverProtector = handshakers.getServer().createFrameProtector(alloc); + + sendMessage(clientProtector, serverProtector, 1, EXAMPLE_MESSAGE1, ref); + sendMessage(serverProtector, clientProtector, 1, EXAMPLE_MESSAGE2, ref); + + clientProtector.destroy(); + serverProtector.destroy(); + } + + /** Ping pong test with small frame size. */ + public static void pingPongSmallFrameTest( + int frameProtectorOverhead, Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + // We send messages using small non-aligned buffers. We use 3 and 5, small primes. + TsiFrameProtector clientProtector = + handshakers.getClient().createFrameProtector(frameProtectorOverhead + 3, alloc); + TsiFrameProtector serverProtector = + handshakers.getServer().createFrameProtector(frameProtectorOverhead + 5, alloc); + + sendMessage(clientProtector, serverProtector, EXAMPLE_MESSAGE1_LEN, EXAMPLE_MESSAGE1, ref); + sendMessage(serverProtector, clientProtector, EXAMPLE_MESSAGE2_LEN, EXAMPLE_MESSAGE2, ref); + + clientProtector.destroy(); + serverProtector.destroy(); + } + + /** Ping pong test with small frame and small buffer. */ + public static void pingPongSmallFrameSmallBufferTest( + int frameProtectorOverhead, Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + // We send messages using small non-aligned buffers. We use 3 and 5, small primes. + TsiFrameProtector clientProtector = + handshakers.getClient().createFrameProtector(frameProtectorOverhead + 3, alloc); + TsiFrameProtector serverProtector = + handshakers.getServer().createFrameProtector(frameProtectorOverhead + 5, alloc); + + sendMessage(clientProtector, serverProtector, EXAMPLE_MESSAGE1_LEN, EXAMPLE_MESSAGE1, ref); + sendMessage(serverProtector, clientProtector, EXAMPLE_MESSAGE2_LEN, EXAMPLE_MESSAGE2, ref); + + sendMessage(clientProtector, serverProtector, EXAMPLE_MESSAGE1_LEN, EXAMPLE_MESSAGE1, ref); + sendMessage(serverProtector, clientProtector, EXAMPLE_MESSAGE2_LEN, EXAMPLE_MESSAGE2, ref); + + clientProtector.destroy(); + serverProtector.destroy(); + } + + /** Test corrupted counter. */ + public static void corruptedCounterTest(Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + TsiFrameProtector sender = handshakers.getClient().createFrameProtector(alloc); + TsiFrameProtector receiver = handshakers.getServer().createFrameProtector(alloc); + + String message = "hello world"; + ByteBuf plaintextBuffer = Unpooled.wrappedBuffer(message.getBytes(UTF_8)); + final List protectOut = new ArrayList<>(); + List unprotectOut = new ArrayList<>(); + + sender.protectFlush( + Collections.singletonList(plaintextBuffer), + new Consumer() { + @Override + public void accept(ByteBuf buf) { + protectOut.add(buf); + } + }, + alloc); + assertThat(protectOut.size()).isEqualTo(1); + + ByteBuf protect = ref.register(protectOut.get(0)); + // Unprotect once to increase receiver counter. + receiver.unprotect(protect.slice(), unprotectOut, alloc); + assertThat(unprotectOut.size()).isEqualTo(1); + ref.register((ByteBuf) unprotectOut.get(0)); + + try { + receiver.unprotect(protect, unprotectOut, alloc); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_RE); + } + + sender.destroy(); + receiver.destroy(); + } + + /** Test corrupted ciphertext. */ + public static void corruptedCiphertextTest(Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + TsiFrameProtector sender = handshakers.getClient().createFrameProtector(alloc); + TsiFrameProtector receiver = handshakers.getServer().createFrameProtector(alloc); + + String message = "hello world"; + ByteBuf plaintextBuffer = Unpooled.wrappedBuffer(message.getBytes(UTF_8)); + final List protectOut = new ArrayList<>(); + List unprotectOut = new ArrayList<>(); + + sender.protectFlush( + Collections.singletonList(plaintextBuffer), + new Consumer() { + @Override + public void accept(ByteBuf buf) { + protectOut.add(buf); + } + }, + alloc); + assertThat(protectOut.size()).isEqualTo(1); + + ByteBuf protect = ref.register(protectOut.get(0)); + int ciphertextIdx = protect.writerIndex() - FakeChannelCrypter.getTagBytes() - 2; + protect.setByte(ciphertextIdx, protect.getByte(ciphertextIdx) + 1); + + try { + receiver.unprotect(protect, unprotectOut, alloc); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_RE); + } + + sender.destroy(); + receiver.destroy(); + } + + /** Test corrupted tag. */ + public static void corruptedTagTest(Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + TsiFrameProtector sender = handshakers.getClient().createFrameProtector(alloc); + TsiFrameProtector receiver = handshakers.getServer().createFrameProtector(alloc); + + String message = "hello world"; + ByteBuf plaintextBuffer = Unpooled.wrappedBuffer(message.getBytes(UTF_8)); + final List protectOut = new ArrayList<>(); + List unprotectOut = new ArrayList<>(); + + sender.protectFlush( + Collections.singletonList(plaintextBuffer), + new Consumer() { + @Override + public void accept(ByteBuf buf) { + protectOut.add(buf); + } + }, + alloc); + assertThat(protectOut.size()).isEqualTo(1); + + ByteBuf protect = ref.register(protectOut.get(0)); + int tagIdx = protect.writerIndex() - 1; + protect.setByte(tagIdx, protect.getByte(tagIdx) + 1); + + try { + receiver.unprotect(protect, unprotectOut, alloc); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_RE); + } + + sender.destroy(); + receiver.destroy(); + } + + /** Test reflected ciphertext. */ + public static void reflectedCiphertextTest(Handshakers handshakers, RegisterRef ref) + throws GeneralSecurityException { + performHandshake(DEFAULT_TRANSPORT_BUFFER_SIZE, handshakers); + + TsiFrameProtector sender = handshakers.getClient().createFrameProtector(alloc); + TsiFrameProtector receiver = handshakers.getServer().createFrameProtector(alloc); + + String message = "hello world"; + ByteBuf plaintextBuffer = Unpooled.wrappedBuffer(message.getBytes(UTF_8)); + final List protectOut = new ArrayList<>(); + List unprotectOut = new ArrayList<>(); + + sender.protectFlush( + Collections.singletonList(plaintextBuffer), + new Consumer() { + @Override + public void accept(ByteBuf buf) { + protectOut.add(buf); + } + }, + alloc); + assertThat(protectOut.size()).isEqualTo(1); + + ByteBuf protect = ref.register(protectOut.get(0)); + try { + sender.unprotect(protect.slice(), unprotectOut, alloc); + fail("Exception expected"); + } catch (AEADBadTagException ex) { + assertThat(ex).hasMessageThat().containsMatch(DECRYPTION_FAILURE_RE); + } + + sender.destroy(); + receiver.destroy(); + } +} diff --git a/android-interop-testing/README.md b/android-interop-testing/README.md index a00752c442e..bd7841e7e84 100644 --- a/android-interop-testing/README.md +++ b/android-interop-testing/README.md @@ -3,8 +3,6 @@ gRPC Android test App Implements gRPC integration tests in an Android App. -TODO(madongfly) integrate this App into the gRPC-Java build system. - In order to build this app, you need a local.properties file under this directory which specifies the location of your android sdk: ``` @@ -35,21 +33,19 @@ $ ../gradlew installDebug Then manually test it with the UI. -Commandline test +Instrumentation tests ---------------- -Run the test with arguments: -``` -$ adb shell am instrument -w -e server_host -e server_port -e server_host_override foo.test.google.fr -e use_tls true -e use_test_ca true -e test_case all io.grpc.android.integrationtest/.TesterInstrumentation -``` +Instrumentation tests must be run on a connected device or emulator. Run with the +following gradle command: -If the test passed successfully, it will output: ``` -INSTRUMENTATION_RESULT: grpc test result=Succeed!!! -INSTRUMENTATION_CODE: 0 -``` -otherwise, output something like: -``` -INSTRUMENTATION_RESULT: grpc test result=Failed... : -INSTRUMENTATION_CODE: 1 +$ ../gradlew connectedAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.server_host=10.0.2.2 \ + -Pandroid.testInstrumentationRunnerArguments.server_port=8080 \ + -Pandroid.testInstrumentationRunnerArguments.use_tls=true \ + -Pandroid.testInstrumentationRunnerArguments.server_host_override=foo.test.google.fr \ + -Pandroid.testInstrumentationRunnerArguments.use_test_ca=true \ + -Pandroid.testInstrumentationRunnerArguments.test_case=all ``` + diff --git a/android-interop-testing/app/build.gradle b/android-interop-testing/app/build.gradle deleted file mode 100644 index 587b2e0fc35..00000000000 --- a/android-interop-testing/app/build.gradle +++ /dev/null @@ -1,75 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'com.google.protobuf' - -android { - compileSdkVersion 22 - buildToolsVersion '22.0.1' - - defaultConfig { - applicationId "io.grpc.android.integrationtest" - minSdkVersion 9 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - } - buildTypes { - debug { - minifyEnabled false - } - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - lintOptions { - disable 'InvalidPackage', 'HardcodedText' - } -} - -protobuf { - protoc { - artifact = 'com.google.protobuf:protoc:3.2.0' - } - plugins { - grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.2.0-SNAPSHOT' // CURRENT_GRPC_VERSION - } - } - generateProtoTasks { - all().each { task -> - task.builtins { - javanano { - // Options added to --javanano_out - option 'ignore_services=true' - } - } - - task.plugins { - grpc { - // Options added to --grpc_out - option 'nano' - } - } - } - } -} - -dependencies { - compile 'com.android.support:appcompat-v7:22.1.1' - compile 'com.google.android.gms:play-services-base:7.3.0' - compile 'com.google.code.findbugs:jsr305:3.0.0' - compile 'com.google.guava:guava:18.0' - compile 'com.squareup.okhttp:okhttp:2.2.0' - // You need to build grpc-java to obtain these libraries below. - compile 'io.grpc:grpc-protobuf-nano:1.2.0-SNAPSHOT' // CURRENT_GRPC_VERSION - compile 'io.grpc:grpc-okhttp:1.2.0-SNAPSHOT' // CURRENT_GRPC_VERSION - compile 'io.grpc:grpc-stub:1.2.0-SNAPSHOT' // CURRENT_GRPC_VERSION - compile 'io.grpc:grpc-testing:1.2.0-SNAPSHOT' // CURRENT_GRPC_VERSION - compile 'javax.annotation:javax.annotation-api:1.2' -} - -gradle.projectsEvaluated { - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:deprecation" - } -} diff --git a/android-interop-testing/app/proguard-rules.pro b/android-interop-testing/app/proguard-rules.pro deleted file mode 100644 index 1c926cf2ce5..00000000000 --- a/android-interop-testing/app/proguard-rules.pro +++ /dev/null @@ -1,20 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in $ANDROID_HOME/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - --dontwarn com.google.common.** --dontwarn okio.** --dontwarn org.mockito.** --dontwarn sun.reflect.** --dontwarn android.test.** -# Ignores: can't find referenced class javax.lang.model.element.Modifier --dontwarn com.google.errorprone.annotations.** --keep class io.grpc.internal.DnsNameResolverProvider --keep class io.grpc.okhttp.OkHttpChannelProvider diff --git a/android-interop-testing/app/src/main/AndroidManifest.xml b/android-interop-testing/app/src/main/AndroidManifest.xml deleted file mode 100644 index 8bb746cb31d..00000000000 --- a/android-interop-testing/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/android-interop-testing/app/src/main/java/io/grpc/android/integrationtest/InteropTester.java b/android-interop-testing/app/src/main/java/io/grpc/android/integrationtest/InteropTester.java deleted file mode 100644 index 5c24d9a68ef..00000000000 --- a/android-interop-testing/app/src/main/java/io/grpc/android/integrationtest/InteropTester.java +++ /dev/null @@ -1,767 +0,0 @@ -/* - * Copyright 2015, Google Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.grpc.android.integrationtest; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.fail; - -import com.google.protobuf.nano.EmptyProtos; -import com.google.protobuf.nano.MessageNano; - -import android.os.AsyncTask; -import android.util.Log; - -import io.grpc.CallOptions; -import io.grpc.ClientCall; -import io.grpc.ManagedChannel; -import io.grpc.Metadata; -import io.grpc.StatusRuntimeException; -import io.grpc.android.integrationtest.nano.Messages; -import io.grpc.android.integrationtest.nano.Messages.Payload; -import io.grpc.android.integrationtest.nano.Messages.ResponseParameters; -import io.grpc.android.integrationtest.nano.Messages.SimpleRequest; -import io.grpc.android.integrationtest.nano.Messages.SimpleResponse; -import io.grpc.android.integrationtest.nano.Messages.StreamingInputCallRequest; -import io.grpc.android.integrationtest.nano.Messages.StreamingInputCallResponse; -import io.grpc.android.integrationtest.nano.Messages.StreamingOutputCallRequest; -import io.grpc.android.integrationtest.nano.Messages.StreamingOutputCallResponse; -import io.grpc.android.integrationtest.nano.TestServiceGrpc; -import io.grpc.android.integrationtest.nano.UnimplementedServiceGrpc; -import io.grpc.stub.StreamObserver; -import io.grpc.testing.StreamRecorder; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.RuntimeException; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * Implementation of the integration tests, as an AsyncTask. - */ -final class InteropTester extends AsyncTask { - static final String SUCCESS_MESSAGE = "Succeed!!!"; - static final String LOG_TAG = "GrpcTest"; - - private ManagedChannel channel; - private TestServiceGrpc.TestServiceBlockingStub blockingStub; - private TestServiceGrpc.TestServiceStub asyncStub; - private String testCase; - private TestListener listener; - private static int TIMEOUT_MILLIS = 5000; - - class ResponseObserver implements StreamObserver { - public LinkedBlockingQueue responses = new LinkedBlockingQueue(); - final Object magicTailResponse = new Object(); - - @Override - public void onNext(Messages.StreamingOutputCallResponse value) { - responses.add(value); - } - - @Override - public void onError(Throwable t) { - Log.e(LOG_TAG, "Encounter an error", t); - responses.add(t); - } - - @Override - public void onCompleted() { - responses.add(magicTailResponse); - } - } - - - public InteropTester(String testCase, - ManagedChannel channel, - TestListener listener) { - this.testCase = testCase; - this.listener = listener; - this.channel = channel; - blockingStub = TestServiceGrpc.newBlockingStub(channel); - asyncStub = TestServiceGrpc.newStub(channel); - } - - @Override - protected void onPreExecute() { - listener.onPreTest(); - } - - @Override - protected String doInBackground(Void... nothing) { - try { - runTest(testCase); - return SUCCESS_MESSAGE; - } catch (Throwable t) { - // Print the stack trace to logcat. - t.printStackTrace(); - // Then print to the error message. - StringWriter sw = new StringWriter(); - t.printStackTrace(new PrintWriter(sw)); - return "Failed... : " + t.getMessage() + "\n" + sw.toString(); - } finally { - shutdown(); - } - } - - @Override - protected void onPostExecute(String result) { - listener.onPostTest(result); - } - - - public void shutdown() { - channel.shutdown(); - } - - public void runTest(String testCase) throws Exception { - Log.i(LOG_TAG, "Running test " + testCase); - if ("all".equals(testCase)) { - runTest("empty_unary"); - runTest("large_unary"); - runTest("client_streaming"); - runTest("server_streaming"); - runTest("ping_pong"); - runTest("empty_stream"); - runTest("cancel_after_begin"); - runTest("cancel_after_first_response"); - runTest("full_duplex_call_should_succeed"); - runTest("half_duplex_call_should_succeed"); - runTest("server_streaming_should_be_flow_controlled"); - runTest("very_large_request"); - runTest("very_large_response"); - runTest("deadline_not_exceeded"); - runTest("deadline_exceeded"); - runTest("deadline_exceeded_server_streaming"); - runTest("unimplemented_method"); - runTest("timeout_on_sleeping_server"); - // This has to be the last one, because it will shut down the channel. - runTest("graceful_shutdown"); - } else if ("empty_unary".equals(testCase)) { - emptyUnary(); - } else if ("large_unary".equals(testCase)) { - largeUnary(); - } else if ("client_streaming".equals(testCase)) { - clientStreaming(); - } else if ("server_streaming".equals(testCase)) { - serverStreaming(); - } else if ("ping_pong".equals(testCase)) { - pingPong(); - } else if ("empty_stream".equals(testCase)) { - emptyStream(); - } else if ("cancel_after_begin".equals(testCase)) { - cancelAfterBegin(); - } else if ("cancel_after_first_response".equals(testCase)) { - cancelAfterFirstResponse(); - } else if ("full_duplex_call_should_succeed".equals(testCase)) { - fullDuplexCallShouldSucceed(); - } else if ("half_duplex_call_should_succeed".equals(testCase)) { - halfDuplexCallShouldSucceed(); - } else if ("server_streaming_should_be_flow_controlled".equals(testCase)) { - serverStreamingShouldBeFlowControlled(); - } else if ("very_large_request".equals(testCase)) { - veryLargeRequest(); - } else if ("very_large_response".equals(testCase)) { - veryLargeResponse(); - } else if ("deadline_not_exceeded".equals(testCase)) { - deadlineNotExceeded(); - } else if ("deadline_exceeded".equals(testCase)) { - deadlineExceeded(); - } else if ("deadline_exceeded_server_streaming".equals(testCase)) { - deadlineExceededServerStreaming(); - } else if ("unimplemented_method".equals(testCase)) { - unimplementedMethod(); - } else if ("timeout_on_sleeping_server".equals(testCase)) { - timeoutOnSleepingServer(); - } else if ("graceful_shutdown".equals(testCase)) { - gracefulShutdown(); - } else { - throw new IllegalArgumentException("Unimplemented/Unknown test case: " + testCase); - } - } - - public void emptyUnary() { - assertMessageEquals(new EmptyProtos.Empty(), blockingStub.emptyCall(new EmptyProtos.Empty())); - } - - public void largeUnary() { - if (shouldSkip()) { - return; - } - final Messages.SimpleRequest request = new Messages.SimpleRequest(); - request.responseSize = 314159; - request.responseType = Messages.COMPRESSABLE; - request.payload = new Payload(); - request.payload.body = new byte[271828]; - - final Messages.SimpleResponse goldenResponse = new Messages.SimpleResponse(); - goldenResponse.payload = new Payload(); - goldenResponse.payload.body = new byte[314159]; - Messages.SimpleResponse response = blockingStub.unaryCall(request); - assertMessageEquals(goldenResponse, response); - } - - public void serverStreaming() throws Exception { - final Messages.StreamingOutputCallRequest request = new Messages.StreamingOutputCallRequest(); - request.responseType = Messages.COMPRESSABLE; - request.responseParameters = new Messages.ResponseParameters[4]; - for (int i = 0; i < 4; i++) { - request.responseParameters[i] = new Messages.ResponseParameters(); - } - request.responseParameters[0].size = 31415; - request.responseParameters[1].size = 9; - request.responseParameters[2].size = 2653; - request.responseParameters[3].size = 58979; - - final Messages.StreamingOutputCallResponse[] goldenResponses = - new Messages.StreamingOutputCallResponse[4]; - for (int i = 0; i < 4; i++) { - goldenResponses[i] = new Messages.StreamingOutputCallResponse(); - goldenResponses[i].payload = new Payload(); - goldenResponses[i].payload.type = Messages.COMPRESSABLE; - } - goldenResponses[0].payload.body = new byte[31415]; - goldenResponses[1].payload.body = new byte[9]; - goldenResponses[2].payload.body = new byte[2653]; - goldenResponses[3].payload.body = new byte[58979]; - - StreamRecorder recorder = StreamRecorder.create(); - asyncStub.streamingOutputCall(request, recorder); - assertTrue(recorder.awaitCompletion(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertSuccess(recorder); - assertMessageEquals(Arrays.asList(goldenResponses), recorder.getValues()); - } - - public void clientStreaming() throws Exception { - final Messages.StreamingInputCallRequest[] requests = new Messages.StreamingInputCallRequest[4]; - for (int i = 0; i < 4; i++) { - requests[i] = new Messages.StreamingInputCallRequest(); - requests[i].payload = new Payload(); - } - requests[0].payload.body = new byte[27182]; - requests[1].payload.body = new byte[8]; - requests[2].payload.body = new byte[1828]; - requests[3].payload.body = new byte[45904]; - - final Messages.StreamingInputCallResponse goldenResponse = - new Messages.StreamingInputCallResponse(); - goldenResponse.aggregatedPayloadSize = 74922; - - StreamRecorder responseObserver = StreamRecorder.create(); - StreamObserver requestObserver = - asyncStub.streamingInputCall(responseObserver); - for (Messages.StreamingInputCallRequest request : requests) { - requestObserver.onNext(request); - } - requestObserver.onCompleted(); - assertMessageEquals(goldenResponse, responseObserver.firstValue().get()); - } - - public void pingPong() throws Exception { - final Messages.StreamingOutputCallRequest[] requests = - new Messages.StreamingOutputCallRequest[4]; - for (int i = 0; i < 4; i++) { - requests[i] = new Messages.StreamingOutputCallRequest(); - requests[i].responseParameters = new Messages.ResponseParameters[1]; - requests[i].responseParameters[0] = new Messages.ResponseParameters(); - requests[i].payload = new Payload(); - } - requests[0].responseParameters[0].size = 31415; - requests[0].payload.body = new byte[27182]; - requests[1].responseParameters[0].size = 9; - requests[1].payload.body = new byte[8]; - requests[2].responseParameters[0].size = 2653; - requests[2].payload.body = new byte[1828]; - requests[3].responseParameters[0].size = 58979; - requests[3].payload.body = new byte[45904]; - - - final Messages.StreamingOutputCallResponse[] goldenResponses = - new Messages.StreamingOutputCallResponse[4]; - for (int i = 0; i < 4; i++) { - goldenResponses[i] = new Messages.StreamingOutputCallResponse(); - goldenResponses[i].payload = new Payload(); - goldenResponses[i].payload.type = Messages.COMPRESSABLE; - } - goldenResponses[0].payload.body = new byte[31415]; - goldenResponses[1].payload.body = new byte[9]; - goldenResponses[2].payload.body = new byte[2653]; - goldenResponses[3].payload.body = new byte[58979]; - - @SuppressWarnings("unchecked") - ResponseObserver responseObserver = new ResponseObserver(); - StreamObserver requestObserver - = asyncStub.fullDuplexCall(responseObserver); - for (int i = 0; i < requests.length; i++) { - requestObserver.onNext(requests[i]); - Object response = responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - if (!(response instanceof Messages.StreamingOutputCallResponse)) { - fail("Unexpected: " + response); - } - assertMessageEquals(goldenResponses[i], (Messages.StreamingOutputCallResponse) response); - assertTrue("More than 1 responses received for ping pong test.", - responseObserver.responses.isEmpty()); - } - requestObserver.onCompleted(); - assertEquals(responseObserver.magicTailResponse, - responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - } - - public void emptyStream() throws Exception { - @SuppressWarnings("unchecked") - ResponseObserver responseObserver = new ResponseObserver(); - StreamObserver requestObserver - = asyncStub.fullDuplexCall(responseObserver); - requestObserver.onCompleted(); - assertEquals(responseObserver.magicTailResponse, - responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - } - - public void cancelAfterBegin() throws Exception { - StreamRecorder responseObserver = StreamRecorder.create(); - StreamObserver requestObserver = - asyncStub.streamingInputCall(responseObserver); - requestObserver.onError(new RuntimeException()); - assertTrue(responseObserver.awaitCompletion(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(Arrays.asList(), responseObserver.getValues()); - assertCodeEquals(io.grpc.Status.CANCELLED, - io.grpc.Status.fromThrowable(responseObserver.getError())); - } - - public void cancelAfterFirstResponse() throws Exception { - final StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.responseParameters = new Messages.ResponseParameters[1]; - request.responseParameters[0] = new ResponseParameters(); - request.responseParameters[0].size = 31415; - request.payload = new Payload(); - request.payload.body = new byte[27182]; - final StreamingOutputCallResponse goldenResponse = new StreamingOutputCallResponse(); - goldenResponse.payload = new Payload(); - goldenResponse.payload.type = Messages.COMPRESSABLE; - goldenResponse.payload.body = new byte[31415]; - - ResponseObserver responseObserver = new ResponseObserver(); - StreamObserver requestObserver - = asyncStub.fullDuplexCall(responseObserver); - requestObserver.onNext(request); - Object response = responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - if (!(response instanceof Messages.StreamingOutputCallResponse)) { - fail("Unexpected: " + response); - } - assertMessageEquals(goldenResponse, (Messages.StreamingOutputCallResponse) response); - - requestObserver.onError(new RuntimeException()); - response = responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - if (!(response instanceof Throwable)) { - fail("Unexpected: " + response); - } - assertCodeEquals(io.grpc.Status.CANCELLED, io.grpc.Status.fromThrowable((Throwable) response)); - } - - public void fullDuplexCallShouldSucceed() throws Exception { - // Build the request. - Integer[] responseSizes = {50, 100, 150, 200}; - final StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.responseParameters = new ResponseParameters[responseSizes.length]; - request.responseType = Messages.COMPRESSABLE; - for (int i = 0; i < responseSizes.length; ++i) { - request.responseParameters[i] = new ResponseParameters(); - request.responseParameters[i].size = responseSizes[i]; - request.responseParameters[i].intervalUs = 0; - } - - StreamRecorder recorder = StreamRecorder.create(); - StreamObserver requestStream = - asyncStub.fullDuplexCall(recorder); - - final int numRequests = 10; - for (int ix = numRequests; ix > 0; --ix) { - requestStream.onNext(request); - } - requestStream.onCompleted(); - assertTrue(recorder.awaitCompletion(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertSuccess(recorder); - assertEquals(responseSizes.length * numRequests, recorder.getValues().size()); - for (int ix = 0; ix < recorder.getValues().size(); ++ix) { - StreamingOutputCallResponse response = recorder.getValues().get(ix); - assertEquals(Messages.COMPRESSABLE, response.payload.type); - int length = response.payload.body.length; - int expectedSize = responseSizes[ix % responseSizes.length]; - assertEquals("comparison failed at index " + ix, expectedSize, length); - } - } - - public void halfDuplexCallShouldSucceed() throws Exception { - // Build the request. - Integer[] responseSizes = {50, 100, 150, 200}; - final StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.responseParameters = new ResponseParameters[responseSizes.length]; - request.responseType = Messages.COMPRESSABLE; - for (int i = 0; i < responseSizes.length; ++i) { - request.responseParameters[i] = new ResponseParameters(); - request.responseParameters[i].size = responseSizes[i]; - request.responseParameters[i].intervalUs = 0; - } - - StreamRecorder recorder = StreamRecorder.create(); - StreamObserver requestStream = asyncStub.halfDuplexCall(recorder); - - final int numRequests = 10; - for (int ix = numRequests; ix > 0; --ix) { - requestStream.onNext(request); - } - requestStream.onCompleted(); - assertTrue(recorder.awaitCompletion(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertSuccess(recorder); - assertEquals(responseSizes.length * numRequests, recorder.getValues().size()); - for (int ix = 0; ix < recorder.getValues().size(); ++ix) { - StreamingOutputCallResponse response = recorder.getValues().get(ix); - assertEquals(Messages.COMPRESSABLE, response.payload.type); - int length = response.payload.body.length; - int expectedSize = responseSizes[ix % responseSizes.length]; - assertEquals("comparison failed at index " + ix, expectedSize, length); - } - } - - public void serverStreamingShouldBeFlowControlled() throws Exception { - final StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.responseType = Messages.COMPRESSABLE; - request.responseParameters = new ResponseParameters[2]; - request.responseParameters[0] = new ResponseParameters(); - request.responseParameters[0].size = 100000; - request.responseParameters[1] = new ResponseParameters(); - request.responseParameters[1].size = 100001; - final StreamingOutputCallResponse[] goldenResponses = new StreamingOutputCallResponse[2]; - goldenResponses[0] = new StreamingOutputCallResponse(); - goldenResponses[0].payload = new Payload(); - goldenResponses[0].payload.type = Messages.COMPRESSABLE; - goldenResponses[0].payload.body = new byte[100000]; - goldenResponses[1] = new StreamingOutputCallResponse(); - goldenResponses[1].payload = new Payload(); - goldenResponses[1].payload.type = Messages.COMPRESSABLE; - goldenResponses[1].payload.body = new byte[100001]; - - long start = System.nanoTime(); - - final ArrayBlockingQueue queue = new ArrayBlockingQueue(10); - ClientCall call = - channel.newCall(TestServiceGrpc.METHOD_STREAMING_OUTPUT_CALL, CallOptions.DEFAULT); - call.start(new ClientCall.Listener() { - @Override - public void onHeaders(Metadata headers) {} - - @Override - public void onMessage(final StreamingOutputCallResponse message) { - queue.add(message); - } - - @Override - public void onClose(io.grpc.Status status, Metadata trailers) { - queue.add(status); - } - }, new Metadata()); - call.sendMessage(request); - call.halfClose(); - - // Time how long it takes to get the first response. - call.request(1); - assertMessageEquals(goldenResponses[0], - (StreamingOutputCallResponse) queue.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - long firstCallDuration = System.nanoTime() - start; - - // Without giving additional flow control, make sure that we don't get another response. We wait - // until we are comfortable the next message isn't coming. We may have very low nanoTime - // resolution (like on Windows) or be using a testing, in-process transport where message - // handling is instantaneous. In both cases, firstCallDuration may be 0, so round up sleep time - // to at least 1ms. - assertNull(queue.poll(Math.max(firstCallDuration * 4, 1 * 1000 * 1000), TimeUnit.NANOSECONDS)); - - // Make sure that everything still completes. - call.request(1); - assertMessageEquals(goldenResponses[1], - (StreamingOutputCallResponse) queue.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertCodeEquals(io.grpc.Status.OK, - (io.grpc.Status) queue.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - } - - public void veryLargeRequest() throws Exception { - if (shouldSkip()) { - return; - } - final SimpleRequest request = new SimpleRequest(); - request.payload = new Payload(); - request.payload.type = Messages.COMPRESSABLE; - request.payload.body = new byte[unaryPayloadLength()]; - request.responseSize = 10; - request.responseType = Messages.COMPRESSABLE; - final SimpleResponse goldenResponse = new SimpleResponse(); - goldenResponse.payload = new Payload(); - goldenResponse.payload.type = Messages.COMPRESSABLE; - goldenResponse.payload.body = new byte[10]; - - assertMessageEquals(goldenResponse, blockingStub.unaryCall(request)); - } - - public void veryLargeResponse() throws Exception { - if (shouldSkip()) { - return; - } - final SimpleRequest request = new SimpleRequest(); - request.responseSize = unaryPayloadLength(); - request.responseType = Messages.COMPRESSABLE; - - SimpleResponse resp = blockingStub.unaryCall(request); - final SimpleResponse goldenResponse = new SimpleResponse(); - goldenResponse.payload = new Payload(); - goldenResponse.payload.type = Messages.COMPRESSABLE; - goldenResponse.payload.body = new byte[unaryPayloadLength()]; - - assertMessageSizeEquals(goldenResponse, resp); - } - - public void deadlineNotExceeded() { - // warm up the channel and JVM - blockingStub.emptyCall(new EmptyProtos.Empty()); - StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.responseParameters = new ResponseParameters[1]; - request.responseParameters[0] = new ResponseParameters(); - request.responseParameters[0].intervalUs = 0; - TestServiceGrpc.newBlockingStub(channel) - .withDeadlineAfter(10, TimeUnit.SECONDS) - .streamingOutputCall(request); - } - - public void deadlineExceeded() { - // warm up the channel and JVM - blockingStub.emptyCall(new EmptyProtos.Empty()); - TestServiceGrpc.TestServiceBlockingStub stub = TestServiceGrpc.newBlockingStub(channel) - .withDeadlineAfter(10, TimeUnit.MILLISECONDS); - StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.responseParameters = new ResponseParameters[1]; - request.responseParameters[0] = new ResponseParameters(); - request.responseParameters[0].intervalUs = 20000; - try { - stub.streamingOutputCall(request).next(); - fail("Expected deadline to be exceeded"); - } catch (StatusRuntimeException ex) { - assertCodeEquals(io.grpc.Status.DEADLINE_EXCEEDED, ex.getStatus()); - } - } - - public void deadlineExceededServerStreaming() throws Exception { - // warm up the channel and JVM - blockingStub.emptyCall(new EmptyProtos.Empty()); - ResponseParameters responseParameters = new ResponseParameters(); - responseParameters.size = 1; - responseParameters.intervalUs = 10000; - StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.responseType = Messages.COMPRESSABLE; - request.responseParameters = new ResponseParameters[4]; - request.responseParameters[0] = responseParameters; - request.responseParameters[1] = responseParameters; - request.responseParameters[2] = responseParameters; - request.responseParameters[3] = responseParameters; - StreamRecorder recorder = StreamRecorder.create(); - TestServiceGrpc.newStub(channel) - .withDeadlineAfter(30, TimeUnit.MILLISECONDS) - .streamingOutputCall(request, recorder); - assertTrue(recorder.awaitCompletion(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertCodeEquals(io.grpc.Status.DEADLINE_EXCEEDED, - io.grpc.Status.fromThrowable(recorder.getError())); - } - - protected int unaryPayloadLength() { - // 10MiB. - return 10485760; - } - - public void gracefulShutdown() throws Exception { - StreamingOutputCallRequest[] requests = new StreamingOutputCallRequest[3]; - requests[0] = new StreamingOutputCallRequest(); - requests[0].responseParameters = new ResponseParameters[1]; - requests[0].responseParameters[0] = new ResponseParameters(); - requests[0].responseParameters[0].size = 3; - requests[0].payload = new Payload(); - requests[0].payload.body = new byte[2]; - requests[1] = new StreamingOutputCallRequest(); - requests[1].responseParameters = new ResponseParameters[1]; - requests[1].responseParameters[0] = new ResponseParameters(); - requests[1].responseParameters[0].size = 1; - requests[1].payload = new Payload(); - requests[1].payload.body = new byte[7]; - requests[2] = new StreamingOutputCallRequest(); - requests[2].responseParameters = new ResponseParameters[1]; - requests[2].responseParameters[0] = new ResponseParameters(); - requests[2].responseParameters[0].size = 4; - requests[2].payload = new Payload(); - requests[2].payload.body = new byte[1]; - - StreamingOutputCallResponse[] goldenResponses = new StreamingOutputCallResponse[3]; - goldenResponses[0] = new StreamingOutputCallResponse(); - goldenResponses[0].payload = new Payload(); - goldenResponses[0].payload.type = Messages.COMPRESSABLE; - goldenResponses[0].payload.body = new byte[3]; - goldenResponses[1] = new StreamingOutputCallResponse(); - goldenResponses[1].payload = new Payload(); - goldenResponses[1].payload.type = Messages.COMPRESSABLE; - goldenResponses[1].payload.body = new byte[1]; - goldenResponses[2] = new StreamingOutputCallResponse(); - goldenResponses[2].payload = new Payload(); - goldenResponses[2].payload.type = Messages.COMPRESSABLE; - goldenResponses[2].payload.body = new byte[4]; - - - ResponseObserver responseObserver = new ResponseObserver(); - StreamObserver requestObserver - = asyncStub.fullDuplexCall(responseObserver); - requestObserver.onNext(requests[0]); - Object response = responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - assertTrue(response instanceof Messages.StreamingOutputCallResponse); - assertMessageEquals(goldenResponses[0], (Messages.StreamingOutputCallResponse) response); - // Initiate graceful shutdown. - channel.shutdown(); - // The previous ping-pong could have raced with the shutdown, but this one certainly shouldn't. - requestObserver.onNext(requests[1]); - response = responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - assertTrue(response instanceof Messages.StreamingOutputCallResponse); - assertMessageEquals(goldenResponses[1], (Messages.StreamingOutputCallResponse) response); - requestObserver.onNext(requests[2]); - response = responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - assertTrue(response instanceof Messages.StreamingOutputCallResponse); - assertMessageEquals(goldenResponses[2], (Messages.StreamingOutputCallResponse) response); - requestObserver.onCompleted(); - assertEquals(responseObserver.magicTailResponse, - responseObserver.responses.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - } - - /** Sends an rpc to an unimplemented method on the server. */ - public void unimplementedMethod() { - UnimplementedServiceGrpc.UnimplementedServiceBlockingStub stub = - UnimplementedServiceGrpc.newBlockingStub(channel); - try { - stub.unimplementedCall(new EmptyProtos.Empty()); - fail(); - } catch (StatusRuntimeException e) { - assertCodeEquals(io.grpc.Status.UNIMPLEMENTED, e.getStatus()); - } - } - - /** Start a fullDuplexCall which the server will not respond, and verify the deadline expires. */ - public void timeoutOnSleepingServer() throws Exception { - TestServiceGrpc.TestServiceStub stub = TestServiceGrpc.newStub(channel) - .withDeadlineAfter(1, TimeUnit.MILLISECONDS); - StreamRecorder recorder = StreamRecorder.create(); - StreamObserver requestObserver - = stub.fullDuplexCall(recorder); - - try { - StreamingOutputCallRequest request = new StreamingOutputCallRequest(); - request.payload = new Messages.Payload(); - request.payload.body = new byte[27182]; - requestObserver.onNext(request); - } catch (IllegalStateException expected) { - // This can happen if the stream has already been terminated due to deadline exceeded. - } - - assertTrue(recorder.awaitCompletion(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertCodeEquals(io.grpc.Status.DEADLINE_EXCEEDED, - io.grpc.Status.fromThrowable(recorder.getError())); - } - - public static void assertMessageSizeEquals(MessageNano expected, MessageNano actual) { - assertEquals(expected.getSerializedSize(), actual.getSerializedSize()); - } - - - private static void assertSuccess(StreamRecorder recorder) { - if (recorder.getError() != null) { - throw new AssertionError(recorder.getError()); - } - } - - public static void assertMessageEquals(MessageNano expected, MessageNano actual) { - if (!MessageNano.messageNanoEquals(expected, actual)) { - assertEquals(expected.toString(), actual.toString()); - fail("Messages not equal, but assertEquals didn't throw"); - } - } - - public static void assertMessageEquals(List expected, - List actual) { - if (expected == null || actual == null) { - assertEquals(expected, actual); - } else if (expected.size() != actual.size()) { - assertEquals(expected, actual); - } else { - for (int i = 0; i < expected.size(); i++) { - assertMessageEquals(expected.get(i), actual.get(i)); - } - } - } - - private static void assertCodeEquals(io.grpc.Status expected, io.grpc.Status actual) { - if (expected == null) { - fail("expected should not be null"); - } - if (actual == null || !expected.getCode().equals(actual.getCode())) { - assertEquals(expected, actual); - } - } - - public interface TestListener { - void onPreTest(); - - void onPostTest(String result); - } - - /** - * Some tests run on memory constrained environments. Rather than OOM, just give up. 64 is - * choosen as a maximum amount of memory a large test would need. - */ - private static boolean shouldSkip() { - Runtime r = Runtime.getRuntime(); - long usedMem = r.totalMemory() - r.freeMemory(); - long actuallyFreeMemory = r.maxMemory() - usedMem; - long wantedFreeMemory = 64 * 1024 * 1024; - if (actuallyFreeMemory < wantedFreeMemory) { - Log.i(LOG_TAG, "Skipping due to lack of memory. " + - "Have: " + actuallyFreeMemory + " Want: " + wantedFreeMemory); - return true; - } - return false; - } -} diff --git a/android-interop-testing/app/src/main/java/io/grpc/android/integrationtest/TesterActivity.java b/android-interop-testing/app/src/main/java/io/grpc/android/integrationtest/TesterActivity.java deleted file mode 100644 index c02e016ecce..00000000000 --- a/android-interop-testing/app/src/main/java/io/grpc/android/integrationtest/TesterActivity.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2015, Google Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.grpc.android.integrationtest; - -import com.google.android.gms.security.ProviderInstaller; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; - -import java.util.LinkedList; -import java.util.List; - -public class TesterActivity extends AppCompatActivity - implements ProviderInstaller.ProviderInstallListener { - private List