From 662c0d579826976105df2ab6d4976c3e0527e0fd Mon Sep 17 00:00:00 2001 From: Henry Story Date: Wed, 24 Mar 2021 00:01:07 +0100 Subject: [PATCH] First step to getting Akka to work with HttpSig --- build.sbt | 1 + src/main/scala/run/cosy/Solid.scala | 6 +- .../scala/run/cosy/http/auth/HttpSig.scala | 71 ++++++++++ .../run/cosy/ldp/fs/BasicContainer.scala | 18 +-- .../run/cosy/http/auth/TestHttpSigRSAFn.scala | 127 ++++++++++++++++++ .../run/cosy/ldp/TestSolidRouteSpec.scala | 3 +- .../run/cosy/ldp/fs/DirectoryListSpec.scala | 4 +- 7 files changed, 212 insertions(+), 18 deletions(-) create mode 100644 src/main/scala/run/cosy/http/auth/HttpSig.scala create mode 100644 src/test/scala/run/cosy/http/auth/TestHttpSigRSAFn.scala diff --git a/build.sbt b/build.sbt index feaecc0..b720fc4 100644 --- a/build.sbt +++ b/build.sbt @@ -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)), diff --git a/src/main/scala/run/cosy/Solid.scala b/src/main/scala/run/cosy/Solid.scala index 32f4ff6..8cb1052 100644 --- a/src/main/scala/run/cosy/Solid.scala +++ b/src/main/scala/run/cosy/Solid.scala @@ -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? - + } /** @@ -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) diff --git a/src/main/scala/run/cosy/http/auth/HttpSig.scala b/src/main/scala/run/cosy/http/auth/HttpSig.scala new file mode 100644 index 0000000..e07a8ee --- /dev/null +++ b/src/main/scala/run/cosy/http/auth/HttpSig.scala @@ -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) + ??? + } + +} diff --git a/src/main/scala/run/cosy/ldp/fs/BasicContainer.scala b/src/main/scala/run/cosy/ldp/fs/BasicContainer.scala index 1616f59..ee8ae9a 100644 --- a/src/main/scala/run/cosy/ldp/fs/BasicContainer.scala +++ b/src/main/scala/run/cosy/ldp/fs/BasicContainer.scala @@ -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) @@ -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: .\n", "@prefix 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}; @@ -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( @@ -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) @@ -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] = diff --git a/src/test/scala/run/cosy/http/auth/TestHttpSigRSAFn.scala b/src/test/scala/run/cosy/http/auth/TestHttpSigRSAFn.scala new file mode 100644 index 0000000..2db1946 --- /dev/null +++ b/src/test/scala/run/cosy/http/auth/TestHttpSigRSAFn.scala @@ -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) + } + + +} diff --git a/src/test/scala/run/cosy/ldp/TestSolidRouteSpec.scala b/src/test/scala/run/cosy/ldp/TestSolidRouteSpec.scala index aef3257..12d9f7b 100644 --- a/src/test/scala/run/cosy/ldp/TestSolidRouteSpec.scala +++ b/src/test/scala/run/cosy/ldp/TestSolidRouteSpec.scala @@ -146,7 +146,6 @@ class TestSolidRouteSpec extends AnyWordSpec with Matchers with ScalatestRouteTe } } - override def afterAll(): Unit = () - deleteDir(dirPath) + override def afterAll(): Unit = deleteDir(dirPath) } diff --git a/src/test/scala/run/cosy/ldp/fs/DirectoryListSpec.scala b/src/test/scala/run/cosy/ldp/fs/DirectoryListSpec.scala index e068ec4..cbca05d 100644 --- a/src/test/scala/run/cosy/ldp/fs/DirectoryListSpec.scala +++ b/src/test/scala/run/cosy/ldp/fs/DirectoryListSpec.scala @@ -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)