aboutsummaryrefslogtreecommitdiff
path: root/gpg/skeybase/src/main/scala/com/github/jodersky/skeybase/Verifier.scala
blob: ccf03086ab8c85f5f82b74ae982a1343caa42d63 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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())
    }
  }*/

}