Skip to content
This repository has been archived by the owner on Feb 10, 2021. It is now read-only.

Implement Signature: header verification for Travis #55

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ sudo: false
language: scala
scala:
- 2.11.8
jdk:
- oraclejdk8
script:
- sbt ++2.11.8 test
# avoid unnecessary cache updates
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,22 @@ savage {
// The HMAC is used to verify that Savage is really being contacted by GitHub,
// and not by some random hacker.
github-web-hook-secret-key = abcdefg
// Used as a shared secret in a hashing scheme that's used to verify
// that Savage is really being contacted by Travis CI,
// Travis's public RSA key.
// Used to verify the signatures of Webhook requests from Travis,
// to ensure that Savage is really being contacted by Travis CI,
// and not by some random hacker. For how to find your Travis token,
// see http://docs.travis-ci.com/user/notifications/#Authorization-for-Webhooks
travis-token = abcdefg
// See https://docs.travis-ci.com/user/notifications/#Verifying-Webhook-requests
// If you're using travis-ci.org, then the key is the value of
// config.notifications.webhook.public_key on https://api.travis-ci.org/config
travis-public-key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtjdLkS+FP+0fPC09j25
y/PiuYDDivIT86COVedvlElk99BBYTrqNaJybxjXbIZ1Q6xFNhOY+iTcBr4E1zJu
tizF3Xi0V9tOuP/M8Wn4Y/1lCWbQKlWrNQuqNBmhovF4K3mDCYswVbpgTmp+JQYu
Bm9QMdieZMNry5s6aiMA9aSjDlNyedvSENYo18F+NYg1J0C0JiPYTxheCb4optr1
5xNzFKhAkuGs4XTOA5C7Q06GCKtDNf44s/CVE30KODUxBi0MCKaxiXw/yy55zxX2
/YdGphIyQiA5iO1986ZmZCLLW8udz9uhW5jUr3Jlp9LbmphAC61bVSf4ou2YsJaN
0QIDAQAB
-----END PUBLIC KEY-----"""
}
```

Expand Down
2 changes: 1 addition & 1 deletion SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ If you're want to run Savage in [Docker](https://www.docker.com), you might want
10. Edit `src/main/resources/application.conf` to configure your instance appropriately. See [the README](https://github.com/twbs/savage/blob/master/README.md#usage) for descriptions of all the available settings.
11. Build Savage's all-in-one "assembly" JAR. See ["How do I generate a single self-sufficient JAR that includes all of the necessary dependencies?"](https://github.com/twbs/savage/blob/master/CONTRIBUTING.md#how-do-i-generate-a-single-self-sufficient-jar-that-includes-all-of-the-necessary-dependencies) for instructions on how to do that.
12. Copy the Savage JAR to the location where you'll be running it on your server.
13. Install Java 7+, Git, and OpenSSH in the environment where you will be running Savage.
13. Install Java 8+, Git, and OpenSSH in the environment where you will be running Savage.
14. Create a Unix user account for Savage. Use whatever username you want.
15. As Savage's Unix user, run `ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts` to grab and confirm GitHub's SSH public key.
16. Generate an SSH key for Savage's Unix user. (This will be used to securely pull-from/push-to GitHub.)
Expand Down
10 changes: 9 additions & 1 deletion src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,13 @@ savage {
username = twbs-savage
password = XXXXXXXX
github-web-hook-secret-key = abcdefg
travis-token = abcdefg
travis-public-key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtjdLkS+FP+0fPC09j25
y/PiuYDDivIT86COVedvlElk99BBYTrqNaJybxjXbIZ1Q6xFNhOY+iTcBr4E1zJu
tizF3Xi0V9tOuP/M8Wn4Y/1lCWbQKlWrNQuqNBmhovF4K3mDCYswVbpgTmp+JQYu
Bm9QMdieZMNry5s6aiMA9aSjDlNyedvSENYo18F+NYg1J0C0JiPYTxheCb4optr1
5xNzFKhAkuGs4XTOA5C7Q06GCKtDNf44s/CVE30KODUxBi0MCKaxiXw/yy55zxX2
/YdGphIyQiA5iO1986ZmZCLLW8udz9uhW5jUr3Jlp9LbmphAC61bVSf4ou2YsJaN
0QIDAQAB
-----END PUBLIC KEY-----"""
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ sealed trait SignatureVerificationStatus
object SuccessfullyVerified extends SignatureVerificationStatus

trait FailedVerification extends SignatureVerificationStatus
object FailedVerification extends SignatureVerificationStatus
object FailedVerification extends FailedVerification
case class ExceptionDuringVerification(error: Throwable) extends FailedVerification
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class SavageWebService(
path("travis") {
pathEndOrSingleSlash {
post {
authenticatedTravisEvent(travisToken = settings.TravisToken, repo = settings.TestRepoId, log = log) { event =>
authenticatedTravisEvent(travisPublicKey = settings.TravisPublicKey, testRepo = settings.TestRepoId, log = log) { event =>
SavageBranch(event.branchName) match {
case Some(branch@SavageBranch(prNum, _)) => {
branchDeleter ! branch
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/com/getbootstrap/savage/server/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import akka.actor.ExtensionIdProvider
import akka.actor.ExtendedActorSystem
import akka.util.ByteString
import org.eclipse.egit.github.core.RepositoryId
import com.getbootstrap.savage.crypto.RsaPublicKey
import com.getbootstrap.savage.github.Branch
import com.getbootstrap.savage.util.{FilePathWhitelist,FilePathWatchlist,Utf8String,RichConfig}

Expand All @@ -19,7 +20,7 @@ class SettingsImpl(config: Config) extends Extension {
val BotUsername: String = config.getString("savage.username")
val BotPassword: String = config.getString("savage.password")
val GitHubWebHookSecretKey: ByteString = ByteString(config.getString("savage.github-web-hook-secret-key").utf8Bytes)
val TravisToken: String = config.getString("savage.travis-token")
val TravisPublicKey: RsaPublicKey = RsaPublicKey.fromPem(config.getString("savage.travis-public-key")).get
val UserAgent: String = config.getString("spray.can.client.user-agent-header")
val DefaultPort: Int = config.getInt("savage.default-port")
val SquelchInvalidHttpLogging: Boolean = config.getBoolean("savage.squelch-invalid-http-logging")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.getbootstrap.savage.server

import java.util.Base64
import scala.util.Try
import akka.event.LoggingAdapter
import spray.http.FormData
import spray.routing.{Directive1, MalformedHeaderRejection, MalformedRequestContentRejection, ValidationRejection}
import spray.routing.directives.{BasicDirectives, HeaderDirectives, RouteDirectives, MarshallingDirectives}
import com.getbootstrap.savage.crypto.{RsaPublicKey, Sha1WithRsa, SuccessfullyVerified}
import com.getbootstrap.savage.util.Utf8String

trait TravisSignatureDirectives {
import BasicDirectives.provide
import HeaderDirectives.headerValueByName
import RouteDirectives.reject
import MarshallingDirectives.{entity, as}

private val signatureHeaderName = "Signature"
private val signatureHeaderValue = headerValueByName(signatureHeaderName)

def travisSignature(log: LoggingAdapter): Directive1[Array[Byte]] = signatureHeaderValue.flatMap { base64 =>
Try{ Base64.getDecoder.decode(base64) }.toOption match {
case Some(bytesFromBase64) => provide(bytesFromBase64)
case None => {
log.error(s"Received Travis request with malformed Base64 value in ${signatureHeaderName} header!")
reject(MalformedHeaderRejection(signatureHeaderName, "Malformed Base64 value"))
}
}
}

private val formDataEntity = entity(as[FormData])

def stringEntityIfTravisSignatureValid(travisPublicKey: RsaPublicKey, log: LoggingAdapter): Directive1[String] = travisSignature(log).flatMap { signature =>
formDataEntity.flatMap { formData =>
formData.fields.toMap.get("payload") match {
case Some(payload:String) => {
Sha1WithRsa.verifySignature(signature = signature, publicKey = travisPublicKey, signedData = payload.utf8Bytes) match {
case SuccessfullyVerified => provide(payload)
case _ => {
log.warning("Received Travis request with incorrect signature! Signature={} Payload={}", signature, payload)
reject(ValidationRejection("Incorrect SHA-1+RSA signature"))
}
}
}
case None => {
log.error("Received Travis request that was missing the `payload` field!")
reject(MalformedRequestContentRejection("Request body form data lacked required `payload` field"))
}
}
}
}
}

object TravisSignatureDirectives extends TravisSignatureDirectives
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,30 @@ import spray.routing.{Directive1, ValidationRejection}
import spray.routing.directives.{BasicDirectives, RouteDirectives}
import spray.json._
import org.eclipse.egit.github.core.RepositoryId
import com.getbootstrap.savage.crypto.RsaPublicKey
import com.getbootstrap.savage.travis.{TravisJsonProtocol, TravisPayload}

trait TravisWebHookDirectives {
import RouteDirectives.reject
import BasicDirectives.provide
import TravisAuthDirectives.stringEntityIfTravisAuthValid
import TravisSignatureDirectives.stringEntityIfTravisSignatureValid
import TravisJsonProtocol._

def authenticatedTravisEvent(travisToken: String, repo: RepositoryId, log: LoggingAdapter): Directive1[TravisPayload] = stringEntityIfTravisAuthValid(travisToken, repo, log).flatMap{ entityJsonString =>
def authenticatedTravisEvent(travisPublicKey: RsaPublicKey, testRepo: RepositoryId, log: LoggingAdapter): Directive1[TravisPayload] = stringEntityIfTravisSignatureValid(travisPublicKey, log).flatMap{ entityJsonString =>
Try { entityJsonString.parseJson.convertTo[TravisPayload] } match {
case Failure(exc) => {
log.error("Received Travis request with bad JSON!")
reject(ValidationRejection("JSON was either malformed or did not match expected schema!"))
}
case Success(payload) => provide(payload)
case Success(payload) => {
val TestRepo = testRepo
payload.repository.id match {
case TestRepo => provide(payload)
case otherRepo => {
reject(ValidationRejection(s"Received Travis request regarding irrelevant repo ${otherRepo}"))
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package com.getbootstrap.savage.travis
import spray.json._

object TravisJsonProtocol extends DefaultJsonProtocol {
implicit val travisPayloadFormat = jsonFormat4(TravisPayload.apply)
implicit val travisRepositoryFormat = jsonFormat2(Repository.apply)
implicit val travisPayloadFormat = jsonFormat5(TravisPayload.apply)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ package com.getbootstrap.savage.travis

import scala.util.{Try,Success,Failure}
import spray.http.Uri
import org.eclipse.egit.github.core.RepositoryId
import com.getbootstrap.savage.github.{Branch, CommitSha}
import com.getbootstrap.savage.travis.build_status.BuildStatus

case class Repository(
owner_name: String,
name: String
) {
def id: RepositoryId = RepositoryId.create(owner_name, name)
}

case class TravisPayload(
status_message: String,
build_url: String,
branch: String,
commit: String
commit: String,
repository: Repository
) {
def status: BuildStatus = BuildStatus(status_message).getOrElse{ throw new IllegalStateException(s"Invalid Travis build status message: ${status_message}") }
def commitSha: CommitSha = CommitSha(commit).getOrElse{ throw new IllegalStateException(s"Invalid commit SHA: ${commit}") }
Expand Down
22 changes: 0 additions & 22 deletions src/main/scala/com/getbootstrap/savage/util/Sha256.scala

This file was deleted.