├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── resources │ └── META-INF │ │ └── gradle-plugins │ │ └── witness.properties │ └── groovy │ └── org │ └── whispersystems │ └── witness │ └── WitnessPlugin.groovy ├── LICENSE ├── gradlew.bat ├── gradlew └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | build 4 | *.iml 5 | local.properties 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlordChain/gradle-witness/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/witness.properties: -------------------------------------------------------------------------------- 1 | implementation-class=org.whispersystems.witness.WitnessPlugin 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Open Whisper Systems 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/main/groovy/org/whispersystems/witness/WitnessPlugin.groovy: -------------------------------------------------------------------------------- 1 | package org.whispersystems.witness 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.api.artifacts.ResolvedArtifact 7 | 8 | import java.security.MessageDigest 9 | 10 | class WitnessPluginExtension { 11 | List verify 12 | } 13 | 14 | class WitnessPlugin implements Plugin { 15 | 16 | static String calculateSha256(file) { 17 | MessageDigest md = MessageDigest.getInstance("SHA-256"); 18 | file.eachByte 4096, {bytes, size -> 19 | md.update(bytes, 0, size); 20 | } 21 | return md.digest().collect {String.format "%02x", it}.join(); 22 | } 23 | 24 | void apply(Project project) { 25 | project.extensions.create("dependencyVerification", WitnessPluginExtension) 26 | project.afterEvaluate { 27 | project.dependencyVerification.verify.each { 28 | assertion -> 29 | List parts = assertion.tokenize(":") 30 | String group = parts.get(0) 31 | String name = parts.get(1) 32 | String hash = parts.get(2) 33 | 34 | ResolvedArtifact dependency = project.configurations.testRuntime.resolvedConfiguration.resolvedArtifacts.find { 35 | return it.name.equals(name) && it.moduleVersion.id.group.equals(group) 36 | } 37 | 38 | println "Verifying " + group + ":" + name 39 | 40 | if (dependency == null) { 41 | throw new InvalidUserDataException("No dependency for integrity assertion found: " + group + ":" + name) 42 | } 43 | 44 | if (!hash.equals(calculateSha256(dependency.file))) { 45 | throw new InvalidUserDataException("Checksum failed for " + assertion) 46 | } 47 | } 48 | } 49 | 50 | 51 | project.task('calculateChecksums') << { 52 | println "dependencyVerification {" 53 | println " verify = [" 54 | 55 | project.configurations.testRuntime.resolvedConfiguration.resolvedArtifacts.each { 56 | dep -> 57 | println " '" + dep.moduleVersion.id.group+ ":" + dep.name + ":" + calculateSha256(dep.file) + "'," 58 | } 59 | 60 | println " ]" 61 | println "}" 62 | } 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradle Witness 2 | 3 | A gradle plugin that enables static verification for remote dependencies. 4 | 5 | Build systems like gradle and maven allow one to specify dependencies for versioned artifacts. An 6 | Android project might list dependencies like this: 7 | 8 | dependency { 9 | compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar' 10 | compile 'com.android.support:support-v4:19.0.1' 11 | compile 'com.google.android.gcm:gcm-client:1.0.2' 12 | compile 'se.emilsjolander:stickylistheaders:2.2.0' 13 | } 14 | 15 | This allows the sample Android project to very easily make use of versioned third party libraries like 16 | [ActionBarSherlock](http://actionbarsherlock.com/), or [StickyListHeaders](https://github.com/emilsjolander/StickyListHeaders). 17 | During the build process, gradle will automatically retrieve the libraries from the configured 18 | maven repositories and incorporate them into the build. This makes it easy to manage dependencies 19 | without having to check jars into a project's source tree. 20 | 21 | ## Dependency Problems 22 | 23 | A "published" maven/gradle artifact [looks like this](https://github.com/WhisperSystems/maven/tree/master/gson/releases/org/whispersystems/gson/2.2.4): 24 | 25 | gson-2.2.4.jar 26 | gson-2.2.4.jar.md5 27 | gson-2.2.4.jar.sha1 28 | gson-2.2.4.pom 29 | gson-2.2.4.pom.md5 30 | gson-2.2.4.pom.sha1 31 | 32 | In the remote directory, the artifact consists of a POM file and a jar or aar, along with md5sum and 33 | sha1sum hash values for those files. 34 | 35 | When gradle retrieves the artifact, it will also retrieve the md5sum and sha1sums to verify that 36 | they match the calculated md5sum and sha1sum of the retrieved files. The problem, obviously, is 37 | that if someone is able to compromise the remote maven repository and change the jar/aar for a 38 | dependency to include some malicious functionality, they could just as easily change the md5sum 39 | and sha1sum values the repository advertises as well. 40 | 41 | ## The Witness Solution 42 | 43 | This gradle plugin simply allows the author of a project to statically specify the sha256sum of 44 | the dependencies that it uses. For our dependency example above, `gradle-witness` would allow 45 | the project to specify: 46 | 47 | dependency { 48 | compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar' 49 | compile 'com.android.support:support-v4:19.0.1' 50 | compile 'com.google.android.gcm:gcm-client:1.0.2' 51 | compile 'se.emilsjolander:stickylistheaders:2.2.0' 52 | } 53 | 54 | dependencyVerification { 55 | verify = [ 56 | 'com.actionbarsherlock:actionbarsherlock:5ab04d74101f70024b222e3ff9c87bee151ec43331b4a2134b6cc08cf8565819', 57 | 'com.android.support:support-v4:a4268abd6370c3fd3f94d2a7f9e6e755f5ddd62450cf8bbc62ba789e1274d585', 58 | 'com.google.android.gcm:gcm-client:5ff578202f93dcba1c210d015deb4241c7cdad9b7867bd1b32e0a5f4c16986ca', 59 | 'se.emilsjolander:stickylistheaders:89146b46c96fea0e40200474a2625cda10fe94891e4128f53cdb42375091b9b6', 60 | ] 61 | } 62 | 63 | The `dependency` definition is the same, but `gradle-witness` allows one to also specify a 64 | `dependencyVerification` definition as well. That definition should include a single list called 65 | `verify` with elements in the format of `group_id:name:sha256sum`. 66 | 67 | At this point, running `gradle build` will first verify that all of the listed dependencies have 68 | the specified sha256sums. If there's a mismatch, the build is aborted. If the remote repository 69 | is later compromised, an attacker won't be able to undetectably modify these artifacts. 70 | 71 | ## Using Witness 72 | 73 | Unfortunately, it doesn't make sense to publish `gradle-witness` as an artifact, since that 74 | creates a bootstrapping problem. To use `gradle-witness`, the jar needs to be built and included 75 | in your project: 76 | 77 | $ git clone https://github.com/WhisperSystems/gradle-witness.git 78 | $ cd gradle-witness 79 | $ gradle build 80 | $ cp build/libs/gradle-witness.jar /path/to/your/project/libs/gradle-witness.jar 81 | 82 | Then in your project's `build.gradle`, the buildscript needs to add a `gradle-witness` dependency. 83 | It might look something like: 84 | 85 | buildscript { 86 | repositories { 87 | mavenCentral() 88 | } 89 | dependencies { 90 | classpath 'com.android.tools.build:gradle:0.9.+' 91 | classpath files('libs/gradle-witness.jar') 92 | } 93 | } 94 | 95 | apply plugin: 'witness' 96 | 97 | At this point you can use `gradle-witness` in your project. If you're feeling "trusting on first 98 | use," you can have `gradle-witness` calculate the sha256sum for all your project's dependencies 99 | (and transitive dependencies!) for you: 100 | 101 | $ gradle -q calculateChecksums 102 | 103 | This will print the full `dependencyVerification` definition to include in the project's `build.gradle`. 104 | For a project that has a dependency definition like: 105 | 106 | dependency { 107 | compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar' 108 | compile 'com.android.support:support-v4:19.0.1' 109 | compile 'com.google.android.gcm:gcm-client:1.0.2' 110 | compile 'se.emilsjolander:stickylistheaders:2.2.0' 111 | } 112 | 113 | Running `gradle -q calculateChecksums` will print: 114 | 115 | dependencyVerification { 116 | verify = [ 117 | 'com.actionbarsherlock:actionbarsherlock:5ab04d74101f70024b222e3ff9c87bee151ec43331b4a2134b6cc08cf8565819', 118 | 'com.android.support:support-v4:a4268abd6370c3fd3f94d2a7f9e6e755f5ddd62450cf8bbc62ba789e1274d585', 119 | 'com.google.android.gcm:gcm-client:5ff578202f93dcba1c210d015deb4241c7cdad9b7867bd1b32e0a5f4c16986ca', 120 | 'se.emilsjolander:stickylistheaders:89146b46c96fea0e40200474a2625cda10fe94891e4128f53cdb42375091b9b6', 121 | ] 122 | } 123 | 124 | ...which you can then include directly below the `dependency` definition in the project's `build.gradle`. 125 | 126 | And that's it! From then on, running a standard `gradle build` will verify the integrity of 127 | the project's dependencies. 128 | --------------------------------------------------------------------------------