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

Commit

Permalink
Implement Signature: header verification for Travis
Browse files Browse the repository at this point in the history
This theoretically addresses #43,
though ideally we should fetch Travis' public key ourselves
rather than requiring the user to copy it into the settings file themself.
  • Loading branch information
cvrebert committed Jan 24, 2017
1 parent d685db6 commit 8b093a9
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 88 deletions.
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
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.

0 comments on commit 8b093a9

Please sign in to comment.