Crypto jwt (#124)

* Introducing CryptoJWT

* JWT logic is removed from Kademlia

* Tiny fixes

* CryptoJwt with dots

* Fixed CryptoJwtSpec for scala.js
This commit is contained in:
Dmitry Kurinskiy 2018-05-21 17:21:44 +03:00 committed by GitHub
parent c27a1865c5
commit 38c6a466f1
3 changed files with 231 additions and 1 deletions

View File

@ -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
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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]
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}