ECDA512 algorithm support (#3953)

* ECDA512 algorithm support

* ECDA512

* happy day test for ECDA512 algorithm

* failure test for wrong key for ECDA512 algorithm

* add ability to use EC cert file

* update docs

* scalafmt

* Correct documentation

CHANGELOG_BEGIN
[Ledger API Authorization] Support elliptic curve algorithm for JWT verification
CHANGELOG_END

Signed-off-by: Brian Healey <brian.healey@digitalasset.com>

* correct docs warning
This commit is contained in:
Brian Healey 2020-01-09 07:25:04 -05:00 committed by GitHub
parent 1d07d82962
commit 95fbf5c921
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 158 additions and 8 deletions

View File

@ -74,6 +74,11 @@ use one of the following command line options:
Both PEM-encoded certificates (text files starting with ``-----BEGIN CERTIFICATE-----``)
and DER-encoded certicates (binary files) are supported.
- ``--auth-jwt-ec-crt=<filename>``.
The sandbox will expect all tokens to be signed with ECDSA512 with the public key loaded from the given X.509 certificate file.
Both PEM-encoded certificates (text files starting with ``-----BEGIN CERTIFICATE-----``)
and DER-encoded certicates (binary files) are supported.
- ``--auth-jwt-rs256-jwks=<url>``.
The sandbox will expect all tokens to be signed with RSA256 with the public key loaded from the given `JWKS <https://tools.ietf.org/html/rfc7517>`__ URL.
@ -112,11 +117,15 @@ where
The ``public`` claim is implicitly held by anyone bearing a valid JWT (even without being an admin or being able to act or read on behalf of any party).
Generating tokens
=================
Generating JSON Web Tokens (JWT)
================================
To generate tokens for testing purposes, use the `jtw.io <https://jwt.io/>`__ web site.
Generating RSA keys
===================
To generate RSA keys for testing purposes, use the following command
.. code-block:: none
@ -128,6 +137,21 @@ which generates the following files:
- ``sandbox.key``: the private key in PEM/DER/PKCS#1 format
- ``sandbox.crt``: a self-signed certificate containing the public key, in PEM/DER/X.509 Certificate format
Generating EC keys
==================
To generate EC (elliptic curve) keys for testing purposes, use the following command
.. code-block:: none
openssl req -x509 -nodes -days 3650 -newkey ec:<(openssl ecparam -name prime256v1) -keyout ecdsa.key -out ecdsa.crt
which generates the following files:
- ``ecdsa.key``: the private key in PEM/DER/PKCS#1 format
- ``ecdsa.crt``: a self-signed certificate containing the public key, in PEM/DER/X.509 Certificate format
Command-line reference
**********************

View File

@ -4,7 +4,7 @@
package com.digitalasset.jwt
import java.nio.charset.Charset
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.{ECPrivateKey, RSAPrivateKey}
import com.auth0.jwt.algorithms.Algorithm
import scalaz.syntax.traverse._
@ -51,6 +51,24 @@ object JwtSigner {
s"${str(base64Jwt.header): String}.${str(base64Jwt.payload)}.${str(base64Signature): String}")
}
object ECDA512 {
def sign(jwt: domain.DecodedJwt[String], privateKey: ECPrivateKey): Error \/ domain.Jwt =
for {
base64Jwt <- base64Encode(jwt)
algorithm <- \/.fromTryCatchNonFatal(Algorithm.ECDSA512(null, privateKey))
.leftMap(e => Error(Symbol("ECDA512.sign"), e.getMessage))
signature <- \/.fromTryCatchNonFatal(algorithm.sign(base64Jwt.header, base64Jwt.payload))
.leftMap(e => Error(Symbol("ECDA512.sign"), e.getMessage))
base64Signature <- base64Encode(signature)
} yield
domain.Jwt(
s"${str(base64Jwt.header): String}.${str(base64Jwt.payload)}.${str(base64Signature): String}")
}
private def str(bs: Array[Byte]) = new String(bs, charset)
@SuppressWarnings(Array("org.wartremover.warts.Any"))

View File

@ -4,11 +4,11 @@
package com.digitalasset.jwt
import java.io.File
import java.security.interfaces.RSAPublicKey
import java.security.interfaces.{ECPublicKey, RSAPublicKey}
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.RSAKeyProvider
import com.auth0.jwt.interfaces.{ECDSAKeyProvider, RSAKeyProvider}
import com.digitalasset.jwt.JwtVerifier.Error
import com.typesafe.scalalogging.StrictLogging
import scalaz.{Show, \/}
@ -60,6 +60,33 @@ object HMAC256Verifier extends StrictLogging {
}.leftMap(e => Error('HMAC256, e.getMessage))
}
// ECDA512 validator factory
object ECDA512Verifier extends StrictLogging {
def apply(keyProvider: ECDSAKeyProvider): Error \/ JwtVerifier =
\/.fromTryCatchNonFatal {
val algorithm = Algorithm.ECDSA512(keyProvider)
val verifier = JWT.require(algorithm).build()
new JwtVerifier(verifier)
}.leftMap(e => Error('ECDA512, e.getMessage))
def apply(publicKey: ECPublicKey): Error \/ JwtVerifier =
\/.fromTryCatchNonFatal {
val algorithm = Algorithm.ECDSA512(publicKey, null)
val verifier = JWT.require(algorithm).build()
new JwtVerifier(verifier)
}.leftMap(e => Error('ECDA512, e.getMessage))
def fromCrtFile(path: String): Error \/ JwtVerifier = {
for {
key <- \/.fromEither(
KeyUtils
.readECPublicKeyFromCrt(new File(path))
.toEither)
.leftMap(e => Error('fromCrtFile, e.getMessage))
verifier <- ECDA512Verifier(key)
} yield verifier
}
}
// RSA256 validator factory
object RSA256Verifier extends StrictLogging {
def apply(publicKey: RSAPublicKey): Error \/ JwtVerifier =

View File

@ -7,7 +7,7 @@ import java.io.{File, FileInputStream}
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.security.cert.CertificateFactory
import java.security.interfaces.{RSAPrivateKey, RSAPublicKey}
import java.security.interfaces.{ECPublicKey, RSAPrivateKey, RSAPublicKey}
import java.security.spec.PKCS8EncodedKeySpec
import java.security.KeyFactory
@ -42,6 +42,21 @@ object KeyUtils {
}
}
/**
* Reads an EC public key from a X509 encoded file.
* These usually have the .crt file extension.
*/
def readECPublicKeyFromCrt(file: File): Try[ECPublicKey] = {
bracket(Try(new FileInputStream(file)))(is => Try(is.close())).flatMap { istream =>
Try(
CertificateFactory
.getInstance("X.509")
.generateCertificate(istream)
.getPublicKey
.asInstanceOf[ECPublicKey])
}
}
/**
* Reads a RSA private key from a PEM/PKCS#8 file.
* These usually have the .pem file extension.

View File

@ -3,7 +3,9 @@
package com.digitalasset.jwt
import java.security.interfaces.{RSAPrivateKey, RSAPublicKey}
import java.security.KeyPair
import java.security.interfaces.{ECPrivateKey, ECPublicKey, RSAPrivateKey, RSAPublicKey}
import java.security.spec.ECGenParameterSpec
import org.scalatest.{Matchers, WordSpec}
import scalaz.syntax.show._
@ -112,5 +114,63 @@ class SignatureSpec extends WordSpec with Matchers {
success.isRight shouldBe true
}
}
"using ECDA512 signatures" should {
"work with a valid key" in {
val kpg = java.security.KeyPairGenerator.getInstance("EC")
val ecGenParameterSpec = new ECGenParameterSpec("secp256r1")
kpg.initialize(ecGenParameterSpec)
val keyPair: KeyPair = kpg.generateKeyPair()
val privateKey = keyPair.getPrivate.asInstanceOf[ECPrivateKey]
val publicKey = keyPair.getPublic.asInstanceOf[ECPublicKey]
val jwtHeader = """{"alg": "ES512", "typ": "JWT"}"""
val jwtPayload = """{"dummy":"dummy"}"""
val jwt = domain.DecodedJwt[String](jwtHeader, jwtPayload)
val success = for {
signedJwt <- JwtSigner.ECDA512
.sign(jwt, privateKey)
.leftMap(e => fail(e.shows))
verifier <- ECDA512Verifier(publicKey)
.leftMap(e => fail(e.shows))
verifiedJwt <- verifier
.verify(signedJwt)
.leftMap(e => fail(e.shows))
} yield verifiedJwt
success.isRight shouldBe true
}
"fail with a invalid key" in {
val kpg = java.security.KeyPairGenerator.getInstance("EC")
val ecGenParameterSpec = new ECGenParameterSpec("secp256r1")
kpg.initialize(ecGenParameterSpec)
val keyPair1: KeyPair = kpg.generateKeyPair()
val privateKey1 = keyPair1.getPrivate.asInstanceOf[ECPrivateKey]
val keyPair2: KeyPair = kpg.generateKeyPair()
val publicKey2 = keyPair2.getPublic.asInstanceOf[ECPublicKey]
val jwtHeader = """{"alg": "ES512", "typ": "JWT"}"""
val jwtPayload = """{"dummy":"dummy"}"""
val jwt = domain.DecodedJwt[String](jwtHeader, jwtPayload)
val success = for {
signedJwt <- JwtSigner.ECDA512
.sign(jwt, privateKey1)
.leftMap(e => fail(e.shows))
verifier <- ECDA512Verifier(publicKey2)
.leftMap(e => fail(e.shows))
error <- verifier
.verify(signedJwt)
.swap
.leftMap(jwt => fail(s"JWT $jwt was unexpectedly verified"))
} yield error
success.isRight shouldBe true
}
}
}
}

View File

@ -8,7 +8,7 @@ import java.time.Duration
import ch.qos.logback.classic.Level
import com.digitalasset.daml.lf.data.Ref
import com.digitalasset.jwt.{HMAC256Verifier, JwksVerifier, RSA256Verifier}
import com.digitalasset.jwt.{ECDA512Verifier, HMAC256Verifier, JwksVerifier, RSA256Verifier}
import com.digitalasset.ledger.api.auth.AuthServiceJWT
import com.digitalasset.ledger.api.domain.LedgerId
import com.digitalasset.ledger.api.tls.TlsConfiguration
@ -168,6 +168,12 @@ object Cli {
.text("Enables JWT-based authorization, where the JWT is signed by RSA256 with a public key loaded from the given X509 certificate file (.crt)")
.action( (path, config) => config.copy(authService = Some(AuthServiceJWT(RSA256Verifier.fromCrtFile(path).valueOr(err => sys.error(s"Failed to create RSA256 verifier: $err"))))))
opt[String]("auth-jwt-ec-crt")
.optional()
.validate(v => Either.cond(v.length > 0, (), "Certificate file path must be a non-empty string"))
.text("Enables JWT-based authorization, where the JWT is signed by ECDA512 with a public key loaded from the given X509 certificate file (.crt)")
.action( (path, config) => config.copy(authService = Some(AuthServiceJWT(ECDA512Verifier.fromCrtFile(path).valueOr(err => sys.error(s"Failed to create ECDA512 verifier: $err"))))))
opt[String]("auth-jwt-rs256-jwks")
.optional()
.validate(v => Either.cond(v.length > 0, (), "JWK server URL must be a non-empty string"))