Skip to content

Commit

Permalink
First step to getting Akka to work with HttpSig
Browse files Browse the repository at this point in the history
  • Loading branch information
bblfish committed Mar 23, 2021
1 parent 6b96787 commit 662c0d5
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 18 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ lazy val root = project
"org.typelevel" %% "cats-free" % "2.4.2",
"net.bblfish.rdf" %% "banana-rdf" % "0.8.5-SNAPSHOT",
"net.bblfish.rdf" %% "banana-jena" % "0.8.5-SNAPSHOT",
"org.tomitribe" % "tomitribe-http-signatures" % "1.7",

//"com.novocode" % "junit-interface" % "0.11" % "test"
).map(_.withDottyCompat(scalaVersion.value)),
Expand Down
6 changes: 1 addition & 5 deletions src/main/scala/run/cosy/Solid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,7 @@ object Solid {
case class StartFailed(cause: Throwable) extends Run
case class Started(binding: ServerBinding) extends Run
case object Stop extends Run

//ActorSystem.t
// where was this?


}

/**
Expand Down Expand Up @@ -154,7 +151,6 @@ class Solid(
reqc.log.info("routing req " + reqc.request.uri)
val (remaining, actor): (List[String], ActorRef[BasicContainer.Cmd]) = registry.getActorRef(path)
.getOrElse((List[String](), rootRef))
println("remaining=" + remaining)
def cmdFn(ref: ActorRef[HttpResponse]): BasicContainer.Cmd = remaining match {
case Nil => BasicContainer.Do(reqc.request,ref)
case head::tail => BasicContainer.Route(NonEmptyList(head,tail), reqc.request, ref)
Expand Down
71 changes: 71 additions & 0 deletions src/main/scala/run/cosy/http/auth/HttpSig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package run.cosy.http.auth

import akka.http.scaladsl.model.{HttpRequest, Uri}
import akka.http.scaladsl.model.headers.{GenericHttpCredentials, HttpChallenge, HttpCredentials, OAuth2BearerToken}
import akka.http.scaladsl.server.{Directive1, RequestContext}
import akka.http.scaladsl.server.Directives.{AsyncAuthenticator, AuthenticationResult, extractCredentials}
import akka.http.scaladsl.server.directives.{AuthenticationDirective, AuthenticationResult, Credentials}
import akka.http.scaladsl.server.directives.BasicDirectives.extractExecutionContext
import akka.http.scaladsl.util.FastFuture
import org.tomitribe.auth.signatures.{Algorithm, Signature, Signer, SigningAlgorithm, Verifier}

import java.security.PublicKey
import java.util
import java.util.Locale
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
import scala.util.matching.Regex

object HttpSig {

trait Agent
case class KeyAgent(keyId: Uri) extends Agent
class Anonymous extends Agent

case class PublicKeyAlgo(pubKey: PublicKey, algo: Algorithm)

val URL: Regex = "<(.*)>".r

/**
* use with [[akka.http.scaladsl.server.directives.SecurityDirectives.authenticateOrRejectWithChallenge]]
*/
def httpSigAuthN(req: HttpRequest)(fetch: Uri => Future[PublicKeyAlgo])(using
ec: ExecutionContext
): Option[HttpCredentials] => Future[AuthenticationResult[Agent]] =
case Some(c@GenericHttpCredentials("Signature",_,params)) =>
// val sig = Try(Signature.fromString(c.toString()))
def p(key: String) = params.get(key)
(p("keyId"),p("algorithm"),p("headers"),p("signature")) match
case (Some(URL(keyId)),Some("hs2019"),Some(headers),Some(sig)) =>
import scala.jdk.CollectionConverters._
fetch(keyId).map{ (pka: PublicKeyAlgo) =>
val signature = new Signature(s"<$keyId>",
SigningAlgorithm.HS2019,pka.algo,null,sig,List(headers.split("\\s+"):_*).asJava)
println("Signature===="+signature.toString)
val ver = new Verifier(pka.pubKey, signature)
val headersMap = req.headers.foldRight(new util.HashMap[String,String]()){(h,m) =>
m.put(h.lowercaseName,h.value); m}
if ver.verify(req.method.value,s"<${req.uri}>",headersMap) then
AuthenticationResult.success(KeyAgent(Uri(keyId)))
else AuthenticationResult.failWithChallenge(HttpChallenge("httpsig",None))
}
case e => //todo: we need to return some more details on the failure
println("Failed because:"+e)
FastFuture.successful(AuthenticationResult.failWithChallenge(HttpChallenge("httpsig",None)))
case None => //we return an anonymous agent
FastFuture.successful(AuthenticationResult.success(Anonymous()))
case _ => //todo: find better way to deal with other Authorization attempts
FastFuture.successful(AuthenticationResult.failWithChallenge(HttpChallenge("httpsig",None)))


def checkSignature(algorithm: Algorithm, pk: PublicKey, sign: List[String]): Unit = {
// val sig: Signature = _
// sig.
// val sig = new Signature("some-key-1", SigningAlgorithm.HS2019, algorithm, null, null, sign.asJava)
// val signer = new Signer(privateKey,sig)
// val signed = signer.sign(method, uri, headers.asJava)
// assertEquals(expected, signed.getSignature)
???
}

}
18 changes: 9 additions & 9 deletions src/main/scala/run/cosy/ldp/fs/BasicContainer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ object BasicContainer {
}

// import java.nio.file.{FileTreeWalker,FileVisitOption}
// def ls(start: Path, options: FileVisitOption*): Source[FileTreeWalker.Event, NotUsed] =
// def ls(start: Path, options: FileVisitOption*): Source[FileTreeWalker.Event, NotUsed] =
// val iterator = new FileTreeIterator(start, 1, options)
// val factory = () => try {
// val spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.DISTINCT)
Expand Down Expand Up @@ -315,21 +315,21 @@ class BasicContainer private(
def urlFor(name: String): Uri = containerUrl.withPath(containerUrl.path / name)

/** Return a Source for reading the relevant files for this directory.
* Note: all symbolic links and dirs are our resources, so long as they
* don't have a `.` in them.
*
* Note: all symbolic links and dirs are our resources, so long as they
* don't have a `.` in them.
*
* todo: now that we have symlinks to archives, we would need to test every symlink for
* what it links to! So we should perhaps instead use a plain file for deleted resources!
* */
val dirList: Source[(Path, BasicFileAttributes), NotUsed] = Source.fromGraph(
DirectoryList(dirPath){ (path: Path, att: BasicFileAttributes) =>
DirectoryList(dirPath,1){ (path: Path, att: BasicFileAttributes) =>
att.isSymbolicLink || (att.isDirectory && !path.getFileName.toString.contains('.'))
})
val prefix: Source[String,NotUsed] = Source(
List("@prefix stat: <http://www.w3.org/ns/posix/stat#> .\n",
"@prefix ldp: <http://www.w3.org/ns/ldp#> .\n\n"))
def containsAsTurtle(path: Path, att: BasicFileAttributes): String = {

def containsAsTurtle(path: Path, att: BasicFileAttributes): String = {
val filename = path.getFileName.toString + { if att.isDirectory then "/" else "" }
s"""<> ldp:contains <$filename> .
| <$filename> stat:size ${att.size};
Expand Down Expand Up @@ -364,6 +364,7 @@ class BasicContainer private(
Behaviors.receiveMessage[Cmd] { (msg: Cmd) =>
import BasicContainer.{ChildTerminated, CreateContainer}
msg match
case act: Do => run(act)
case create: CreateContainer =>
// we don't do much at this point. For later
create.cmd.replyTo ! HttpResponse(
Expand All @@ -373,7 +374,6 @@ class BasicContainer private(
)
)
Behaviors.same
case act: Do => run(act)
case routeMsg: Route => routeHttpReq(routeMsg)
case ChildTerminated(name) =>
reg.removePath(containerUrl.path / name)
Expand Down Expand Up @@ -476,7 +476,7 @@ class BasicContainer private(
} recover {
case e => context.log.warn(s"Can't save counter value $count for <$countFile>", e)
}


protected
def run(msg: Do): Behavior[Cmd] =
Expand Down
127 changes: 127 additions & 0 deletions src/test/scala/run/cosy/http/auth/TestHttpSigRSAFn.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package run.cosy.http.auth

import akka.http.scaladsl.model.HttpHeader.ParsingResult.Ok
import akka.http.scaladsl.model.headers.{Authorization, HttpCredentials}
import akka.http.scaladsl.model.{HttpHeader, HttpMethod, HttpMethods, HttpRequest, Uri}
import akka.http.scaladsl.server
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.util.FastFuture
import junit.framework.Assert.fail
import org.tomitribe.auth.signatures.{Algorithm, Signature, Signatures, Signer, Verifier}
import run.cosy.http.auth.HttpSig.{KeyAgent, PublicKeyAlgo}
import run.cosy.ldp.testUtils.TmpDir

import java.nio.file.Path
import java.security.PublicKey
import java.security.spec.AlgorithmParameterSpec
import scala.collection.immutable.HashMap
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.jdk.CollectionConverters._
import scala.util.Success

class TestHttpSigRSAFn extends munit.FunSuite {

val privateKeyPem: String = "-----BEGIN RSA PRIVATE KEY-----\n" +
"MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF\n" +
"NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F\n" +
"UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB\n" +
"AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA\n" +
"QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK\n" +
"kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg\n" +
"f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u\n" +
"412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc\n" +
"mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7\n" +
"kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA\n" +
"gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW\n" +
"G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI\n" +
"7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==\n" +
"-----END RSA PRIVATE KEY-----\n";

val publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3\n" +
"6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6\n" +
"Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw\n" +
"oYi+1hqp1fIekaxsyQIDAQAB\n" +
"-----END PUBLIC KEY-----\n";

import org.tomitribe.auth.signatures.PEM
import java.io.ByteArrayInputStream

lazy val privateKey = PEM.readPrivateKey(new ByteArrayInputStream(privateKeyPem.getBytes))
lazy val publicKey: PublicKey = PEM.readPublicKey(new ByteArrayInputStream(publicKeyPem.getBytes))

// val signature: Signature = new Signature("key-alias", "hmac-sha256", null, "(request-target)");

import org.tomitribe.auth.signatures.SigningAlgorithm
import java.util

val method = "POST"
val uri = "/foo?param=value&pet=dog"
val headers = Map[String,String](
"Host" -> "example.org",
"Date" -> "Thu, 05 Jan 2012 21:31:40 GMT",
"Content-Type" -> "application/json",
"Digest" -> "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
"Accept" -> "*/*",
"Content-Length" -> "18"
)

test("rsaSha512") {
import org.tomitribe.auth.signatures.Algorithm
val algorithm = Algorithm.RSA_SHA512

assertSignature(algorithm, "IItboA8OJgL8WSAnJa8MND04s9j7d" +
"B6IJIBVpOGJph8Tmkc5yUAYjvO/UQUKytRBe5CSv2GLfTAmE" +
"7SuRgGGMwdQZubNJqRCiVPKBpuA47lXrKgC/wB0QAMkPHI6c" +
"PllBZRixmjZuU9mIbuLjXMHR+v/DZwOHT9k8x0ILUq2rKE=",
List("date"))

assertSignature(algorithm, "ggIa4bcI7q377gNoQ7qVYxTA4pEOl" +
"xlFzRtiQV0SdPam4sK58SFO9EtzE0P1zVTymTnsSRChmFU2p" +
"n+R9VzkAhQ+yEbTqzu+mgHc4P1L5IeeXQ5aAmGENfkRbm2vd" +
"OZzP5j6ruB+SJXIlhnaum2lsuyytSS0m/GkWvFJVZFu33M=",
List("(request-target)", "host", "date"))
}

test("rsaSha512 using httpSigAuthN") {
testReq(Algorithm.RSA_SHA512, List("date"))
testReq(Algorithm.RSA_SHA512, List("host", "date"))
testReq(Algorithm.RSA_SHA512, List("(request-target)", "host", "date"))
}

private def testReq(algorithm: Algorithm, sign: List[String]) = {
import scala.concurrent.duration.*
val sig = new Signature(s"<$uri>", SigningAlgorithm.HS2019, algorithm, null, null, sign.asJava)
val signer = new Signer(privateKey,sig)
val signature = signer.sign("post",s"<$uri>",headers.asJava)

val authorization = "Authorization" -> signature.toString
val hdr: List[HttpHeader] = (authorization::headers.iterator.to(List)).map { (k, v) =>
HttpHeader.parse(k, v) match
case Ok(hdr, List()) => hdr
case e => fail("error:" + e)
}
val req: HttpRequest = HttpRequest(HttpMethods.POST, Uri(uri), hdr)

given ec: ExecutionContext = ExecutionContext.global

val f: Future[server.Directives.AuthenticationResult[HttpSig.Agent]] = HttpSig.httpSigAuthN(req){ url =>
assertEquals(url, Uri(uri))
FastFuture.successful(HttpSig.PublicKeyAlgo(publicKey,algorithm))
}(req.header[Authorization].map(_.credentials))
assertEquals(Await.result(f, 1.second), Right(KeyAgent(Uri(uri))))
}

@throws[Exception]
def assertSignature(algorithm: Algorithm, expected: String, sign: List[String]): Unit = {
val sig = new Signature("some-key-1", SigningAlgorithm.HS2019, algorithm, null, null, sign.asJava)
val signer = new Signer(privateKey,sig)
val signed: Signature = signer.sign(method, uri, headers.asJava)
assertEquals(expected, signed.getSignature)
val verifier = new Verifier(publicKey, signed)
assert(verifier.verify(method,uri,headers.asJava))
//Signature.fromString(signed.getSignature)
}


}
3 changes: 1 addition & 2 deletions src/test/scala/run/cosy/ldp/TestSolidRouteSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ class TestSolidRouteSpec extends AnyWordSpec with Matchers with ScalatestRouteTe
}
}

override def afterAll(): Unit = ()
deleteDir(dirPath)
override def afterAll(): Unit = deleteDir(dirPath)

}
4 changes: 2 additions & 2 deletions src/test/scala/run/cosy/ldp/fs/DirectoryListSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ class DirectoryListSpec extends AkkaSpec {
import akka.stream.stage.GraphStageLogic
import akka.stream.stage.OutHandler

val sourceGraph = DirectoryList(Path.of("."),depth=10)()
val sourceGraph = DirectoryList(Path.of("test"),depth=2)()
val result = Source.fromGraph(sourceGraph).runForeach{ (p: Path,att: BasicFileAttributes) =>
println(s"received <$p> : dir=${att.isDirectory}")
//todo
}

Await.result(result,10.seconds)
Expand Down

0 comments on commit 662c0d5

Please sign in to comment.