aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jodersky@gmail.com>2015-04-27 17:00:53 +0200
committerJakob Odersky <jodersky@gmail.com>2015-04-27 17:00:53 +0200
commit56557445cd959315754e0de1ffbb000eeaf5f08c (patch)
tree41c7ebdb77398c6bcb5d74ebb97f5a2b3cecd3f1
parentf79ee0e3999dfd04af306aced213f20b7f8e0904 (diff)
downloadsecurity-master.tar.gz
security-master.tar.bz2
security-master.zip
refactorings and add website verifierHEADmaster
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Keybase.scala19
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Main.scala13
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/VerificationException.scala8
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Verifier.scala (renamed from gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala)59
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/Backend.scala4
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/openpgp/GnuPG.scala11
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/responses.scala39
-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/statements.scala6
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/GitHubVerifier.scala (renamed from gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala)25
-rw-r--r--gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/WebsiteFileVerifier.scala (renamed from gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala)20
11 files changed, 125 insertions, 83 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
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/verification/Verifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Verifier.scala
index 6025fef..ccf0308 100644
--- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/Verifier.scala
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Verifier.scala
@@ -1,5 +1,4 @@
package com.github.jodersky.skeybase
-package verification
import scala.language.implicitConversions
@@ -8,10 +7,6 @@ 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
@@ -19,42 +14,46 @@ import spray.http.HttpResponse
import spray.http.Uri
import spray.json.DefaultJsonProtocol
import spray.json.JsonParser
-import spray.json.ParserInput.apply
+/** 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 = jsonFormat2(Service.apply)
+ implicit val serviceFormat = jsonFormat4(Service.apply)
implicit val keyFormat = jsonFormat1(PublicKey.apply)
implicit val statementBodyFormat = jsonFormat2(StatementBody.apply)
- implicit val statementFormat = jsonFormat1(Statement.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 RuntimeException("Too many redirects."))
+ 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 RuntimeException("Missing location header in redirect response."))
+ Future.failed(new NoSuchElementException("Missing location header in redirect response."))
}
} else {
Future.successful(request.uri, response)
@@ -65,31 +64,35 @@ object Verifier {
dispatch(request, maxRedirects)
}
- def finalHost(host: String) = (uri: Uri, response: HttpResponse) => {
+ /** 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 is not " + host)
+ throw new VerificationException("Final host " + uri.authority.host.address + " 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 {
+ }).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")
}
}
- 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
-
+ /** 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
+ }
}
/*
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/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/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/verification/GitHubVerifier.scala b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/GitHubVerifier.scala
index 5243b36..dfcaa94 100644
--- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/GitHubVerifier.scala
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/GitHubVerifier.scala
@@ -1,12 +1,7 @@
package com.github.jodersky.skeybase
-package verification
+package verifiers
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
@@ -46,15 +41,21 @@ class GitHubVerifier(backend: Backend) extends Verifier {
throw new NoSuchElementException("No gist found.")
}
}
- val gistPipeline = withRedirects(sendReceive) ~> finalHost("api.github.com").tupled ~> unmarshal[Seq[Gist]] ~> urlOfHeadGist
+ 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"));
- content <- rawPipeline(Get(rawUrl));
- signed <- extractSignedStatement(content);
- clear <- backend.verifySignature(signed, fingerprint);
- verified <- verifyStatement(clear, "github", proof.nametag)
+ 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
}
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/verifiers/WebsiteFileVerifier.scala
index 04cad1e..12d2d73 100644
--- a/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verification/WebsiteFileVerifier.scala
+++ b/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/verifiers/WebsiteFileVerifier.scala
@@ -1,12 +1,7 @@
package com.github.jodersky.skeybase
-package verification
+package verifiers
import scala.concurrent.Future
-
-
-import Verifier.extractSignedStatement
-import Verifier.finalHost
-import Verifier.verifyStatement
import Verifier.withRedirects
import akka.actor.ActorSystem
import openpgp.Backend
@@ -24,12 +19,19 @@ class WebsiteFileVerifier(backend: Backend) extends Verifier {
def verify(fingerprint: String, proof: Proof)(implicit sys: ActorSystem) = {
import sys.dispatcher
- val pipeline = withRedirects(sendReceive) ~> finalHost(proof.nametag).tupled ~> unmarshal[String]
+ 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 <- extractSignedStatement(content);
+ signed <- extractSignedMessage(content);
clear <- backend.verifySignature(signed, fingerprint);
- verified <- verifyStatement(clear, "github", proof.nametag)
+ verified <- verifyOwnershipStatement(clear, service)
) yield {
proof
}