From 38c6a466f17232b9e13b52b2b18e6913f314dacc Mon Sep 17 00:00:00 2001 From: Dmitry Kurinskiy Date: Mon, 21 May 2018 17:21:44 +0300 Subject: [PATCH] Crypto jwt (#124) * Introducing CryptoJWT * JWT logic is removed from Kademlia * Tiny fixes * CryptoJwt with dots * Fixed CryptoJwtSpec for scala.js --- .../fluence/crypto/signature/SignAlgo.scala | 33 ++++- .../scala/fluence/crypto/jwt/CryptoJwt.scala | 117 ++++++++++++++++++ .../fluence/crypto/jwt/CryptoJwtSpec.scala | 82 ++++++++++++ 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 jwt/src/main/scala/fluence/crypto/jwt/CryptoJwt.scala create mode 100644 jwt/src/test/scala/fluence/crypto/jwt/CryptoJwtSpec.scala diff --git a/core/src/main/scala/fluence/crypto/signature/SignAlgo.scala b/core/src/main/scala/fluence/crypto/signature/SignAlgo.scala index 0f0484d..df61b51 100644 --- a/core/src/main/scala/fluence/crypto/signature/SignAlgo.scala +++ b/core/src/main/scala/fluence/crypto/signature/SignAlgo.scala @@ -17,7 +17,14 @@ package fluence.crypto.signature -import fluence.crypto.{Crypto, KeyPair} +import cats.Monad +import cats.data.EitherT +import cats.syntax.strong._ +import cats.syntax.compose._ +import fluence.crypto.{Crypto, CryptoError, KeyPair} +import scodec.bits.ByteVector + +import scala.language.higherKinds /** * Signature algorithm -- cryptographically coupled keypair, signer and signature checker. @@ -38,4 +45,28 @@ object SignAlgo { type SignerFn = KeyPair ⇒ Signer type CheckerFn = KeyPair.Public ⇒ SignatureChecker + + /** + * Take checker, signature, and plain data, and apply checker, returning Unit on success, or left side error. + */ + private val fullChecker: Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] = + new Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] { + override def apply[F[_]: Monad]( + input: ((SignatureChecker, Signature), ByteVector) + ): EitherT[F, CryptoError, Unit] = { + val ((signatureChecker, signature), plainData) = input + signatureChecker.check(signature, plainData) + } + } + + /** + * For CheckerFn, builds a function that takes PubKeyAndSignature along with plain data, and checks the signature. + */ + def checkerFunc(fn: CheckerFn): Crypto.Func[(PubKeyAndSignature, ByteVector), Unit] = + Crypto + .liftFunc[PubKeyAndSignature, (SignatureChecker, Signature)] { + case PubKeyAndSignature(pk, signature) ⇒ fn(pk) -> signature + } + .first[ByteVector] andThen fullChecker + } diff --git a/jwt/src/main/scala/fluence/crypto/jwt/CryptoJwt.scala b/jwt/src/main/scala/fluence/crypto/jwt/CryptoJwt.scala new file mode 100644 index 0000000..ee7c593 --- /dev/null +++ b/jwt/src/main/scala/fluence/crypto/jwt/CryptoJwt.scala @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2017 Fluence Labs Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fluence.crypto.jwt + +import cats.syntax.compose._ +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import fluence.codec.bits.BitsCodecs +import fluence.codec.bits.BitsCodecs._ +import fluence.codec.{CodecError, PureCodec} +import fluence.codec.circe.CirceCodecs +import fluence.crypto.{Crypto, CryptoError, KeyPair} +import fluence.crypto.signature.SignAlgo.CheckerFn +import fluence.crypto.signature.{PubKeyAndSignature, SignAlgo, Signature, Signer} +import io.circe.{Decoder, Encoder} +import scodec.bits.{Bases, ByteVector} + +/** + * Primitivized version of JWT. + * + * @param readPubKey Gets public key from decoded Header and Claim + * @tparam H Header type + * @tparam C Claim type + */ +class CryptoJwt[H: Encoder: Decoder, C: Encoder: Decoder]( + readPubKey: PureCodec.Func[(H, C), KeyPair.Public] +) { + // Public Key reader, with errors lifted to Crypto + private val readPK = Crypto.fromOtherFunc(readPubKey)(Crypto.liftCodecErrorToCrypto) + + private val headerAndClaimCodec = Crypto.codec(CryptoJwt.headerClaimCodec[H, C]) + + private val signatureCodec = Crypto.codec(CryptoJwt.signatureCodec) + + private val stringTripleCodec = Crypto.codec(CryptoJwt.stringTripleCodec) + + // Take a JWT string, parse and deserialize it, check signature, return Header and Claim on success + def reader(checkerFn: CheckerFn): Crypto.Func[String, (H, C)] = + Crypto.liftFuncPoint[String, (H, C)]( + jwtToken ⇒ + for { + triple ← stringTripleCodec.inverse.pointAt(jwtToken) + (hc @ (h, c), s) = triple + headerAndClaim ← headerAndClaimCodec.inverse.pointAt(hc) + pk ← readPK.pointAt(headerAndClaim) + signature ← signatureCodec.inverse.pointAt(s) + plainData = ByteVector((h + c).getBytes()) + _ ← SignAlgo.checkerFunc(checkerFn).pointAt(PubKeyAndSignature(pk, signature) → plainData) + } yield headerAndClaim + ) + + // With the given Signer, serialize Header and Claim into JWT string, signing it on the way + def writer(signer: Signer): Crypto.Func[(H, C), String] = + Crypto.liftFuncPoint[(H, C), String]( + headerAndClaim ⇒ + for { + pk ← readPK.pointAt(headerAndClaim) + _ ← Crypto + .liftFuncEither[Boolean, Unit]( + Either.cond(_, (), CryptoError("JWT encoded PublicKey doesn't match with signer's PublicKey")) + ) + .pointAt(pk == signer.publicKey) + hc ← headerAndClaimCodec.direct.pointAt(headerAndClaim) + plainData = ByteVector((hc._1 + hc._2).getBytes()) + signature ← signer.sign.pointAt(plainData) + s ← signatureCodec.direct.pointAt(signature) + jwtToken ← stringTripleCodec.direct.pointAt((hc, s)) + } yield jwtToken + ) +} + +object CryptoJwt { + + private val alphabet = Bases.Alphabets.Base64Url + + private val strVec = BitsCodecs.base64AlphabetToVector(alphabet).swap + + val signatureCodec: PureCodec[Signature, String] = + PureCodec.liftB[Signature, ByteVector](_.sign, Signature(_)) andThen strVec + + private def jsonCodec[T: Encoder: Decoder]: PureCodec[T, String] = + CirceCodecs.circeJsonCodec[T] andThen + CirceCodecs.circeJsonParseCodec andThen + PureCodec.liftB[String, Array[Byte]](_.getBytes(), new String(_)) andThen + PureCodec[Array[Byte], ByteVector] andThen + strVec + + val stringTripleCodec: PureCodec[((String, String), String), String] = + PureCodec.liftEitherB( + { + case ((a, b), c) ⇒ Right(s"$a.$b.$c") + }, + s ⇒ + s.split('.').toList match { + case a :: b :: c :: Nil ⇒ Right(((a, b), c)) + case l ⇒ Left(CodecError("Wrong number of dot-divided parts, expected: 3, actual: " + l.size)) + } + ) + + def headerClaimCodec[H: Encoder: Decoder, C: Encoder: Decoder]: PureCodec[(H, C), (String, String)] = + jsonCodec[H] split jsonCodec[C] +} diff --git a/jwt/src/test/scala/fluence/crypto/jwt/CryptoJwtSpec.scala b/jwt/src/test/scala/fluence/crypto/jwt/CryptoJwtSpec.scala new file mode 100644 index 0000000..c1eeef0 --- /dev/null +++ b/jwt/src/test/scala/fluence/crypto/jwt/CryptoJwtSpec.scala @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 Fluence Labs Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fluence.crypto.jwt + +import cats.{Id, Monad} +import cats.data.EitherT +import fluence.codec.PureCodec +import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer} +import fluence.crypto.{Crypto, CryptoError, KeyPair} +import io.circe.{Json, JsonNumber, JsonObject} +import org.scalatest.{Matchers, WordSpec} +import scodec.bits.ByteVector + +import scala.language.higherKinds + +class CryptoJwtSpec extends WordSpec with Matchers { + "CryptoJwt" should { + val keys: Seq[KeyPair] = + Stream.from(0).map(ByteVector.fromInt(_)).map(i ⇒ KeyPair.fromByteVectors(i, i)) + + val cryptoJwt = + new CryptoJwt[JsonNumber, JsonObject]( + PureCodec.liftFunc(n ⇒ KeyPair.Public(ByteVector.fromInt(n._1.toInt.get))) + ) + + implicit val checker: SignAlgo.CheckerFn = + publicKey ⇒ + new SignatureChecker { + override def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit] = + EitherT.cond[F](signature.sign == plain.reverse, (), CryptoError("Signatures mismatch")) + } + + val signer: SignAlgo.SignerFn = + keyPair ⇒ Signer(keyPair.publicKey, Crypto.liftFunc(plain ⇒ Signature(plain.reverse))) + + "be a total bijection for valid JWT" in { + val kp = keys.head + val str = cryptoJwt + .writer(signer(kp)) + .unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value. of test")))) + + str.count(_ == '.') shouldBe 2 + + cryptoJwt.reader(checker).unsafe(str)._2.kleisli.run("test").get shouldBe Json.fromString("value. of test") + } + + "fail with wrong signer" in { + val kp = keys.tail.head + val str = cryptoJwt + .writer(signer(kp)) + .runEither[Id]( + (JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value of test"))) + ) + + str.isLeft shouldBe true + } + + "fail with wrong signature" in { + val kp = keys.head + val str = cryptoJwt + .writer(signer(kp)) + .unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value of test")))) + + cryptoJwt.reader(checker).runEither[Id](str + "m").isLeft shouldBe true + } + } +}