mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-10 10:46:11 +03:00
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:
parent
1d07d82962
commit
95fbf5c921
@ -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
|
||||
**********************
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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 =
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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"))
|
||||
|
Loading…
Reference in New Issue
Block a user