aboutsummaryrefslogtreecommitdiff
path: root/gpg/skeybase/src/main/scala/com/github
diff options
context:
space:
mode:
Diffstat (limited to 'gpg/skeybase/src/main/scala/com/github')
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Keybase.scala61
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Main.scala39
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/Backend.scala18
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/GnuPG.scala58
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/responses.scala7
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala64
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/VerificationException.scala4
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala109
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala39
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/statements.scala6
10 files changed, 405 insertions, 0 deletions
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Keybase.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Keybase.scala
new file mode 100644
index 0000000..bfc246e
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Keybase.scala
@@ -0,0 +1,61 @@
+package com.github.jodersky.skeybase
+
+import scala.concurrent.Future
+
+import akka.actor.ActorSystem
+import spray.client.pipelining.Get
+import spray.client.pipelining.WithTransformerConcatenation
+import spray.client.pipelining.sendReceive
+import spray.client.pipelining.sendReceive$default$3
+import spray.client.pipelining.unmarshal
+import spray.httpx.SprayJsonSupport.sprayJsonUnmarshaller
+import spray.json.DefaultJsonProtocol
+import spray.json.DeserializationException
+import spray.json.JsArray
+import spray.json.JsObject
+import spray.json.JsValue
+import spray.json.RootJsonFormat
+
+object Keybase {
+ def origin = new Keybase("https://keybase.io")
+
+ object JsonProtocol extends DefaultJsonProtocol {
+ implicit val basicsFormat = jsonFormat1(Basics.apply)
+ implicit val proofFormat = jsonFormat(Proof.apply, "nametag", "proof_type", "proof_url")
+
+ implicit object PrimaryKeyFormat extends RootJsonFormat[PublicKey] {
+ def write(key: PublicKey) = throw new NotImplementedError
+ def read(value: JsValue) = value.asJsObject.getFields("primary") match {
+ case Seq(JsObject(data)) => data.get("key_fingerprint") map (f => PublicKey(f.convertTo[String].toUpperCase())) getOrElse {
+ throw new DeserializationException("Fingerprint expected")
+ }
+ case _ => throw new DeserializationException("Primary key expected")
+ }
+ }
+
+ implicit object ProofsFormat extends RootJsonFormat[Seq[Proof]] {
+ def write(proofs: Seq[Proof]) = throw new NotImplementedError
+ def read(value: JsValue) = value.asJsObject.getFields("all") match {
+ case Seq(JsArray(values)) => values.map(_.convertTo[Proof])
+ case _ => throw new DeserializationException("Proofs array expected")
+ }
+ }
+ implicit val userFormat = jsonFormat(User.apply, "basics", "proofs_summary", "public_keys")
+ implicit val lookupFormat = jsonFormat1(LookupResponse.apply)
+ }
+}
+
+class Keybase(host: String) {
+
+ import Keybase.JsonProtocol._
+
+ def lookup(username: String)(implicit system: ActorSystem): Future[User] = {
+ import system.dispatcher
+
+ val lookup = sendReceive ~> unmarshal[LookupResponse]
+ val url = host + "/_/api/1.0/user/lookup.json?usernames=" + username + "&fields=proofs_summary,public_keys"
+
+ lookup(Get(url)).map(_.them.head)
+ }
+
+} \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Main.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Main.scala
new file mode 100644
index 0000000..1553b96
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Main.scala
@@ -0,0 +1,39 @@
+package com.github.jodersky.skeybase
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+import akka.actor.ActorSystem
+import scala.language.implicitConversions
+import scala.util.Success
+import scala.util.Failure
+import openpgp.GnuPG
+import verification.GitHubVerifier
+import verification.VerificationException
+
+object Main {
+
+ def main(args: Array[String]): Unit = {
+ implicit val system = ActorSystem()
+ import system.dispatcher
+
+ val verifier = new GitHubVerifier(new GnuPG())
+
+ val proofs = for (
+ user <- Keybase.origin.lookup("jodersky");
+ github = user.proofs.find(_.proofType == "github").get;
+ verification <- verifier.verify(user.key.fingerprint, github)
+ ) yield {
+ verification
+ }
+
+ proofs onComplete { result =>
+ result match {
+ case Success(proof) => println("done")
+ case Failure(err: VerificationException) => println("Verification exception! Someone may be doing something nasty.")
+ case Failure(err) => err.printStackTrace()
+ }
+ system.shutdown()
+ }
+ }
+
+} \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/Backend.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/Backend.scala
new file mode 100644
index 0000000..108ee00
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/Backend.scala
@@ -0,0 +1,18 @@
+package com.github.jodersky.skeybase
+package openpgp
+
+import scala.util.Try
+
+trait Backend {
+
+ /** Imports a key into this backend so that it is available for verification. */
+ def importKey(key: String): Unit
+
+ /**
+ * verifies a signed statement.
+ * @param signed the statement to verify
+ * @param fingerprint the fingerprint of the key that allegedly signed this statement
+ */
+ def verifySignature(signed: String, fingerprint: String): Try[String]
+
+} \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/GnuPG.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/GnuPG.scala
new file mode 100644
index 0000000..3b2d152
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/GnuPG.scala
@@ -0,0 +1,58 @@
+package com.github.jodersky.skeybase
+package openpgp
+
+import java.io.File
+import scala.sys.process._
+import java.io.ByteArrayInputStream
+import scala.collection.mutable.ArrayBuffer
+import verification.VerificationException
+import scala.util.Try
+
+class GnuPG(
+ val home: File = new File("."),
+ val command: String = "/usr/bin/gpg")
+ extends Backend {
+
+ import GnuPG._
+
+ private val _gpg = s"${command} --home=${home.getAbsolutePath} --no-default-keyring --keyring=temp.gpg --status-fd=2"
+ private def gpg(args: String) = _gpg + " " + args
+
+ def importKey(key: String) = {
+ val result = (gpg("--import -") #< stream(key)).!
+ result == 0
+ }
+
+ def verifySignature(statement: String, fingerprint: String): Try[String] = Try{
+ val stdout = new StringBuilder
+ val stderr = new ArrayBuffer[String]
+
+ val status = (gpg("-d -") #< stream(statement)) ! ProcessLogger(stdout append _, stderr append _)
+
+ if (status != 0) throw new VerificationException("gpg exited with non-zero exit code")
+
+ /* see doc/DETAILS of GnuPG for more information about structure */
+ def fpr(line: String) = """\[GNUPG:\] VALIDSIG (\S+\s+){9}(\w+)""".r findPrefixMatchOf (line) map { m =>
+ m.group(2)
+ }
+
+ val valid = stderr find (fpr(_) == Some(fingerprint))
+
+ if (valid.isEmpty) {
+ throw new VerificationException("Statement is not signed by the correct key.")
+ } else {
+ stdout.toString()
+ }
+ }
+
+}
+
+object GnuPG {
+
+ private def stream(str: String) = {
+ val bytes = str.getBytes("UTF-8")
+ new ByteArrayInputStream(bytes)
+ }
+
+ val tmp = "~/.skeybase"
+} \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/responses.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/responses.scala
new file mode 100644
index 0000000..79ff9be
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/responses.scala
@@ -0,0 +1,7 @@
+package com.github.jodersky.skeybase
+
+case class PublicKey(fingerprint: String)
+case class Proof(nametag: String, proofType: String, proofUrl: String)
+case class Basics(username: String)
+case class LookupResponse(them: Seq[User])
+case class User(basics: Basics, proofs: Seq[Proof], key: PublicKey) \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala
new file mode 100644
index 0000000..5243b36
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala
@@ -0,0 +1,64 @@
+package com.github.jodersky.skeybase
+package verification
+
+import scala.concurrent.Future
+
+import Verifier.extractSignedStatement
+import Verifier.finalHost
+import Verifier.verifyStatement
+import Verifier.withRedirects
+import akka.actor.ActorSystem
+import openpgp.Backend
+import spray.client.pipelining.Get
+import spray.client.pipelining.WithTransformerConcatenation
+import spray.client.pipelining.sendReceive
+import spray.client.pipelining.sendReceive$default$3
+import spray.client.pipelining.unmarshal
+import spray.httpx.SprayJsonSupport.sprayJsonUnmarshaller
+import spray.json.DefaultJsonProtocol
+
+object GitHubVerifier {
+ case class GistFile(rawUrl: String)
+ case class Gist(url: String, files: Map[String, GistFile])
+
+ object GitHubProtocol extends DefaultJsonProtocol {
+ implicit val gistFileFormat = jsonFormat(GistFile, "raw_url")
+ implicit val gistFormat = jsonFormat2(Gist)
+ }
+}
+
+class GitHubVerifier(backend: Backend) extends Verifier {
+ import Verifier._
+ import GitHubVerifier._
+ import GitHubVerifier.GitHubProtocol._
+
+ def verify(fingerprint: String, proof: Proof)(implicit sys: ActorSystem) = {
+ import sys.dispatcher
+
+ val urlOfHeadGist = (gists: Seq[Gist]) => {
+ val url = for (
+ gist <- gists.headOption;
+ (_, file) <- gist.files.headOption
+ ) yield {
+ file.rawUrl
+ }
+ url getOrElse {
+ throw new NoSuchElementException("No gist found.")
+ }
+ }
+ val gistPipeline = withRedirects(sendReceive) ~> finalHost("api.github.com").tupled ~> unmarshal[Seq[Gist]] ~> urlOfHeadGist
+ val rawPipeline = sendReceive ~> unmarshal[String]
+
+ for (
+ rawUrl <- gistPipeline(Get("https://api.github.com/users/" + proof.nametag + "/gists"));
+ content <- rawPipeline(Get(rawUrl));
+ signed <- extractSignedStatement(content);
+ clear <- backend.verifySignature(signed, fingerprint);
+ verified <- verifyStatement(clear, "github", proof.nametag)
+ ) yield {
+ proof
+ }
+ }
+
+}
+ \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/VerificationException.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/VerificationException.scala
new file mode 100644
index 0000000..a7c1f78
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/VerificationException.scala
@@ -0,0 +1,4 @@
+package com.github.jodersky.skeybase
+package verification
+
+class VerificationException(message: String) extends RuntimeException(message) \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala
new file mode 100644
index 0000000..6025fef
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala
@@ -0,0 +1,109 @@
+package com.github.jodersky.skeybase
+package verification
+
+import scala.language.implicitConversions
+
+import scala.concurrent.ExecutionContext
+import scala.concurrent.Future
+import scala.util.Failure
+import scala.util.Success
+import scala.util.Try
+
+import com.github.jodersky.skeybase.Proof
+import com.github.jodersky.skeybase.PublicKey
+
+import akka.actor.ActorSystem
+import spray.http.HttpHeaders.Location
+import spray.http.HttpRequest
+import spray.http.HttpResponse
+import spray.http.Uri
+import spray.json.DefaultJsonProtocol
+import spray.json.JsonParser
+import spray.json.ParserInput.apply
+
+trait Verifier {
+
+ def verify(fingerprint: String, proof: Proof)(implicit sys: ActorSystem): Future[Proof]
+
+}
+
+object Verifier {
+
+ object JsonProtocol extends DefaultJsonProtocol {
+ implicit val serviceFormat = jsonFormat2(Service.apply)
+ implicit val keyFormat = jsonFormat1(PublicKey.apply)
+ implicit val statementBodyFormat = jsonFormat2(StatementBody.apply)
+ implicit val statementFormat = jsonFormat1(Statement.apply)
+ }
+ import JsonProtocol._
+
+ implicit def tryToFuture[A](t: Try[A]): Future[A] = t match {
+ case Success(a) => Future.successful(a)
+ case Failure(e) => Future.failed(e)
+ }
+
+ def withRedirects(
+ sendReceive: HttpRequest => Future[HttpResponse],
+ maxRedirects: Int = 5)(implicit ec: ExecutionContext): HttpRequest => Future[(Uri, HttpResponse)] = { request =>
+
+ def dispatch(request: HttpRequest, redirectsLeft: Int): Future[(Uri, HttpResponse)] = if (redirectsLeft <= 0) {
+ Future.failed(new RuntimeException("Too many redirects."))
+ } else {
+ sendReceive(request).flatMap { response =>
+ if (response.status.value.startsWith("3")) {
+ response.header[Location].map { location =>
+ dispatch(request.copy(uri = location.uri), redirectsLeft - 1)
+ } getOrElse {
+ Future.failed(new RuntimeException("Missing location header in redirect response."))
+ }
+ } else {
+ Future.successful(request.uri, response)
+ }
+ }
+ }
+
+ dispatch(request, maxRedirects)
+ }
+
+ def finalHost(host: String) = (uri: Uri, response: HttpResponse) => {
+ if (uri.authority.host.address != host)
+ throw new VerificationException("Final host is not " + host)
+ else
+ response
+ }
+
+ def extractSignedStatement(content: String): Try[String] = Try {
+ val regex = """(-----BEGIN PGP MESSAGE-----(.|\n)*-----END PGP MESSAGE-----?)""".r
+ regex.findFirstIn(content) getOrElse {
+ throw new VerificationException("No OpenPGP message found.")
+ }
+ }
+
+ def verifyStatement(statement: String, service: String, username: String): Try[String] = Try {
+ val stmt = JsonParser(statement).convertTo[Statement]
+
+ if (stmt.body.service.name != service) throw new VerificationException(
+ "The service specified in the signed statement (" + stmt.body.service.name + ") is not " +
+ "the same as the service under which the statement was found (" + service + ")")
+ else if (stmt.body.service.username != username) throw new VerificationException(
+ "The username specified in the signed statement (" + stmt.body.service.username + ") is not " +
+ "the same as the username under which the statement was found (" + username + ")")
+ else statement
+
+ }
+
+ /*
+ * if (!(uri.path.tail startsWith (Path(proof.nametag)))) {
+ * throw new VerificationException("Final github account does not match the one provided in the proof." + uri.path.head)
+ * }
+
+
+ def extractHtmlId(id: String, html: String): Option[String] = {
+ val cleaner = new HtmlCleaner
+ val root = cleaner.clean(html)
+ root.getElementsByName("div", true).find(_.getAttributeByName("id") == id).map { div =>
+ StringEscapeUtils.unescapeHtml4(div.getText.toString())
+ }
+ }*/
+
+} \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala
new file mode 100644
index 0000000..04cad1e
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala
@@ -0,0 +1,39 @@
+package com.github.jodersky.skeybase
+package verification
+
+import scala.concurrent.Future
+
+
+import Verifier.extractSignedStatement
+import Verifier.finalHost
+import Verifier.verifyStatement
+import Verifier.withRedirects
+import akka.actor.ActorSystem
+import openpgp.Backend
+import spray.client.pipelining.Get
+import spray.client.pipelining.WithTransformerConcatenation
+import spray.client.pipelining.sendReceive
+import spray.client.pipelining.sendReceive$default$3
+import spray.client.pipelining.unmarshal
+import spray.httpx.SprayJsonSupport.sprayJsonUnmarshaller
+import spray.json.DefaultJsonProtocol
+
+class WebsiteFileVerifier(backend: Backend) extends Verifier {
+ import Verifier._
+
+ def verify(fingerprint: String, proof: Proof)(implicit sys: ActorSystem) = {
+ import sys.dispatcher
+
+ val pipeline = withRedirects(sendReceive) ~> finalHost(proof.nametag).tupled ~> unmarshal[String]
+ for (
+ content <- pipeline(Get(proof.proofUrl));
+ signed <- extractSignedStatement(content);
+ clear <- backend.verifySignature(signed, fingerprint);
+ verified <- verifyStatement(clear, "github", proof.nametag)
+ ) yield {
+ proof
+ }
+ }
+
+}
+ \ No newline at end of file
diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/statements.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/statements.scala
new file mode 100644
index 0000000..cbe896f
--- /dev/null
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/statements.scala
@@ -0,0 +1,6 @@
+package com.github.jodersky.skeybase
+package verification
+
+case class Service(name: String, username: String)
+case class StatementBody(key: PublicKey, service: Service)
+case class Statement(body: StatementBody) \ No newline at end of file