From 56557445cd959315754e0de1ffbb000eeaf5f08c Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 27 Apr 2015 17:00:53 +0200 Subject: refactorings and add website verifier --- .../com/github/jodersky/skeybase/Keybase.scala | 19 +++- .../scala/com/github/jodersky/skeybase/Main.scala | 13 ++- .../jodersky/skeybase/VerificationException.scala | 8 ++ .../com/github/jodersky/skeybase/Verifier.scala | 112 +++++++++++++++++++++ .../github/jodersky/skeybase/openpgp/Backend.scala | 4 +- .../github/jodersky/skeybase/openpgp/GnuPG.scala | 11 +- .../com/github/jodersky/skeybase/responses.scala | 39 ++++++- .../skeybase/verification/GitHubVerifier.scala | 64 ------------ .../verification/VerificationException.scala | 4 - .../jodersky/skeybase/verification/Verifier.scala | 109 -------------------- .../verification/WebsiteFileVerifier.scala | 39 ------- .../skeybase/verification/statements.scala | 6 -- .../skeybase/verifiers/GitHubVerifier.scala | 65 ++++++++++++ .../skeybase/verifiers/WebsiteFileVerifier.scala | 41 ++++++++ 14 files changed, 288 insertions(+), 246 deletions(-) create mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/VerificationException.scala create mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Verifier.scala delete mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala delete mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/VerificationException.scala delete mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala delete mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala delete mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/statements.scala create mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/GitHubVerifier.scala create mode 100644 gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/WebsiteFileVerifier.scala 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 index bfc246e..4a2e4ba 100644 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Keybase.scala +++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Keybase.scala @@ -17,7 +17,9 @@ import spray.json.JsValue import spray.json.RootJsonFormat object Keybase { - def origin = new Keybase("https://keybase.io") + + //private JSON responses + protected case class LookupResponse(them: Seq[User]) object JsonProtocol extends DefaultJsonProtocol { implicit val basicsFormat = jsonFormat1(Basics.apply) @@ -45,16 +47,23 @@ object Keybase { } } -class Keybase(host: String) { - +/** + * Keybase.io API entry point. + * @param host the internet host implementing the Keybase API, defaults to "https://keybase.io" + */ +class Keybase(host: String = "https://keybase.io") { + import Keybase._ import Keybase.JsonProtocol._ + + private val entry = host + "/_/api/1.0/" + /** Retrieves a user from keybase */ 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" + val url = entry + "user/lookup.json?usernames=" + username + "&fields=proofs_summary,public_keys" + val lookup = sendReceive ~> unmarshal[LookupResponse] lookup(Get(url)).map(_.them.head) } 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 index 1553b96..460e221 100644 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Main.scala +++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Main.scala @@ -7,8 +7,7 @@ import scala.language.implicitConversions import scala.util.Success import scala.util.Failure import openpgp.GnuPG -import verification.GitHubVerifier -import verification.VerificationException +import verifiers._ object Main { @@ -16,12 +15,12 @@ object Main { implicit val system = ActorSystem() import system.dispatcher - val verifier = new GitHubVerifier(new GnuPG()) + val verifier = new WebsiteFileVerifier(new GnuPG()) //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) + user <- new Keybase().lookup("jodersky"); + github = user.proofs.find(_.proofType == "generic_web_site").get; + verification <- verifier.verify(user.primaryKey.fingerprint, github) ) yield { verification } @@ -29,7 +28,7 @@ object Main { 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: VerificationException) => println("Verification exception! Someone may be doing something evil. " + err.getMessage) case Failure(err) => err.printStackTrace() } system.shutdown() diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/VerificationException.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/VerificationException.scala new file mode 100644 index 0000000..228c7c8 --- /dev/null +++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/VerificationException.scala @@ -0,0 +1,8 @@ +package com.github.jodersky.skeybase + +/** + * Thrown when the verification of a proof fails. This exception may only be thrown in case of an error + * during verification (such as an invalid signature), NOT in related circumstances (such as an offline service + * or missing resource). + */ +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/Verifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Verifier.scala new file mode 100644 index 0000000..ccf0308 --- /dev/null +++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Verifier.scala @@ -0,0 +1,112 @@ +package com.github.jodersky.skeybase + +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 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 + +/** Verifies a user identity proof. */ +trait Verifier { + + /** Checks if a given proof is actually signed by the provided key. */ + def verify(fingerprint: String, proof: Proof)(implicit sys: ActorSystem): Future[Proof] + +} + +/** Contains utilities for concrete verifiers. */ +object Verifier { + + object JsonProtocol extends DefaultJsonProtocol { + implicit val serviceFormat = jsonFormat4(Service.apply) + implicit val keyFormat = jsonFormat1(PublicKey.apply) + implicit val statementBodyFormat = jsonFormat2(StatementBody.apply) + implicit val statementFormat = jsonFormat1(OwnershipStatement.apply) + } + import JsonProtocol._ + + /** Convert a try to a future, useful for writing expressive for-comprehensions mixing futures and tries. */ + implicit def tryToFuture[A](t: Try[A]): Future[A] = t match { + case Success(a) => Future.successful(a) + case Failure(e) => Future.failed(e) + } + + /** Pipeline stage that follows redirects and also keeps track of the final URL. */ + 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 UnsupportedOperationException("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 NoSuchElementException("Missing location header in redirect response.")) + } + } else { + Future.successful(request.uri, response) + } + } + } + + dispatch(request, maxRedirects) + } + + /** Pipeline stage that ensures the request's host matches a provided parameter. */ + def finalHost(host: String) = ((uri: Uri, response: HttpResponse) => { + if (uri.authority.host.address != host) + throw new VerificationException("Final host " + uri.authority.host.address + " is not " + host) + else + response + }).tupled + + /** Extract an OpenPGP delimited message from a content. */ + def extractSignedMessage(content: String): Try[String] = Try { + val header = "-----BEGIN PGP MESSAGE-----" + val footer = "-----END PGP MESSAGE-----" + val inner = content.lines.dropWhile(_ != header).takeWhile(_ != footer) + if (inner.isEmpty) { + throw new VerificationException("No OpenPGP message found.") + } else { + (inner ++ Seq(footer)).mkString("\n") + } + } + + /** Verify the contents of a statement of ownership against a known service. */ + def verifyOwnershipStatement(statement: String, goodService: Service): Try[OwnershipStatement] = Try { + val stmt = JsonParser(statement).convertTo[OwnershipStatement] + + if (stmt.body.service != goodService) { + throw new VerificationException("The statement of ownership does not match the required service.") + } else { + stmt + } + } + + /* + * 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/openpgp/Backend.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/Backend.scala index 108ee00..056f863 100644 --- 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 @@ -9,8 +9,8 @@ trait Backend { def importKey(key: String): Unit /** - * verifies a signed statement. - * @param signed the statement to verify + * Verifies a signed message. + * @param signed the message to verify * @param fingerprint the fingerprint of the key that allegedly signed this statement */ def verifySignature(signed: String, fingerprint: String): Try[String] 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 index 3b2d152..befc18c 100644 --- 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 @@ -5,17 +5,16 @@ 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") + val command: String = "gpg") extends Backend { import GnuPG._ - private val _gpg = s"${command} --home=${home.getAbsolutePath} --no-default-keyring --keyring=temp.gpg --status-fd=2" + private val _gpg = s"${command} --home=${home.getAbsolutePath} --no-default-keyring --keyring=keybase.gpg --status-fd=2" private def gpg(args: String) = _gpg + " " + args def importKey(key: String) = { @@ -23,13 +22,13 @@ class GnuPG( result == 0 } - def verifySignature(statement: String, fingerprint: String): Try[String] = Try{ + def verifySignature(message: 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 _) + val status = (gpg("-d -") #< stream(message)) ! ProcessLogger(stdout append _, stderr append _) - if (status != 0) throw new VerificationException("gpg exited with non-zero exit code") + if (status != 0) throw new VerificationException("GnuPG exited with non-zero exit code. Stderr: \n" + stderr.mkString("\n")) /* 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 => 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 index 79ff9be..9d9edf2 100644 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/responses.scala +++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/responses.scala @@ -1,7 +1,38 @@ package com.github.jodersky.skeybase -case class PublicKey(fingerprint: String) -case class Proof(nametag: String, proofType: String, proofUrl: String) +/** A keybase user. */ +case class User(basics: Basics, proofs: Seq[Proof], primaryKey: PublicKey) + +/** Basic information about a user. */ 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 + +/** + * An identity "proof" consisting of an online, cryptographically signed statement asserting + * that a specific username of an online service has control of the signing key. + * @param nametag the name of the user controlling the key + * @param proofType string identifying the kind of proof, i.e. the service or website + * @param proofUrl the URL under which the proof can be found + */ +case class Proof(nametag: String, proofType: String, proofUrl: String) + +/** An OpenPGP public key belonging to a user, represented by its fingerprint*/ +case class PublicKey(fingerprint: String) + +/** + * A statement, usually available in a signed form, that asserts + * the ownership of a public key and service handle (i.e. a username + * or an entire website). + */ +case class OwnershipStatement(body: StatementBody) + +/** + * Actual meaningful contents of an ownership statement. + */ +case class StatementBody(key: PublicKey, service: Service) + +/** + * An online service, e.g. a social media handle or a website. + * Note: since keybase.io are inconsistent with their statements across various services, this class + * provides only optional fields. It is up to verifiers to ensure that the correct fields are set. + */ +case class Service(name: Option[String], username: Option[String], hostname: Option[String], domain: Option[String]) 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 deleted file mode 100644 index 5243b36..0000000 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index a7c1f78..0000000 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/VerificationException.scala +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 6025fef..0000000 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala +++ /dev/null @@ -1,109 +0,0 @@ -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 deleted file mode 100644 index 04cad1e..0000000 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index cbe896f..0000000 --- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/statements.scala +++ /dev/null @@ -1,6 +0,0 @@ -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 diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/GitHubVerifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/GitHubVerifier.scala new file mode 100644 index 0000000..dfcaa94 --- /dev/null +++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/GitHubVerifier.scala @@ -0,0 +1,65 @@ +package com.github.jodersky.skeybase +package verifiers + +import scala.concurrent.Future +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") ~> unmarshal[Seq[Gist]] ~> urlOfHeadGist + val rawPipeline = sendReceive ~> unmarshal[String] + + val githubService = Service( + name = Some("github"), + username = Some(proof.nametag), + hostname = None, + domain = None) + + for ( + rawUrl <- gistPipeline(Get("https://api.github.com/users/" + proof.nametag + "/gists")); // url of raw gist + content <- rawPipeline(Get(rawUrl)); // content of raw gist + signed <- extractSignedMessage(content); // signed statement of ownership + clear <- backend.verifySignature(signed, fingerprint); // verified against fingerprint + verified <- verifyOwnershipStatement(clear, githubService) // verified statement + ) yield { + proof + } + } + +} + \ No newline at end of file diff --git a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/WebsiteFileVerifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/WebsiteFileVerifier.scala new file mode 100644 index 0000000..12d2d73 --- /dev/null +++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/WebsiteFileVerifier.scala @@ -0,0 +1,41 @@ +package com.github.jodersky.skeybase +package verifiers + +import scala.concurrent.Future +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) ~> unmarshal[String] + + val service = Service( + name = None, + username = None, + hostname = Some(proof.nametag), + domain = None) + + for ( + content <- pipeline(Get(proof.proofUrl)); + signed <- extractSignedMessage(content); + clear <- backend.verifySignature(signed, fingerprint); + verified <- verifyOwnershipStatement(clear, service) + ) yield { + proof + } + } + +} + \ No newline at end of file -- cgit v1.2.3