From d6dac16042dfac83b4ad0bdef6286631012376b6 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 27 Aug 2023 16:56:15 -0700 Subject: [PATCH] Move EC curve utility functions (#803) - Changed EC point multiplication montgomery ladder iteration to be a little faster (still has fixed number of operations of course) - Moved Weierstrass calc/utility methods to ECCurve instead of AbstractEcJwkFactory - Removed unnecessary Curves.java since StandardCurves is the preferred implementation - Renamed CurvesTest to StandardCurvesTest --- .../impl/security/AbstractEcJwkFactory.java | 177 +------------ .../io/jsonwebtoken/impl/security/Curves.java | 79 ------ .../jsonwebtoken/impl/security/ECCurve.java | 241 +++++++++++++++++- .../impl/security/EcPrivateJwkFactory.java | 30 ++- .../impl/security/EcPublicJwkFactory.java | 20 +- .../impl/security/EcSignatureAlgorithm.java | 32 +-- .../impl/security/EcdhKeyAlgorithm.java | 88 +++---- .../impl/security/StandardCurves.java | 21 +- .../groovy/io/jsonwebtoken/JwksCRVTest.groovy | 15 +- .../security/AbstractEcJwkFactoryTest.groovy | 100 +------- .../impl/security/ECCurveTest.groovy | 172 ++++++++++++- .../security/EcPrivateJwkFactoryTest.groovy | 35 +++ .../security/EcPublicJwkFactoryTest.groovy | 14 + .../security/EcSignatureAlgorithmTest.groovy | 2 +- .../impl/security/EcdhKeyAlgorithmTest.groovy | 25 +- .../impl/security/RFC7518AppendixCTest.groovy | 5 +- .../impl/security/RFC7520Section5Test.groovy | 5 +- .../impl/security/RFC8037AppendixATest.groovy | 5 +- ...sTest.groovy => StandardCurvesTest.groovy} | 31 +-- 19 files changed, 616 insertions(+), 481 deletions(-) delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{CurvesTest.groovy => StandardCurvesTest.groovy} (76%) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index 0f5e81cfd..5aac4f7de 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -15,54 +15,26 @@ */ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.security.Curve; import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.UnsupportedKeyException; import java.math.BigInteger; import java.security.Key; -import java.security.KeyFactory; import java.security.interfaces.ECKey; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.ECFieldFp; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.EllipticCurve; -import java.security.spec.InvalidKeySpecException; import java.util.Set; abstract class AbstractEcJwkFactory> extends AbstractFamilyJwkFactory { - private static final BigInteger TWO = BigInteger.valueOf(2); - private static final BigInteger THREE = BigInteger.valueOf(3); - private static final String UNSUPPORTED_CURVE_MSG = "The specified ECKey curve does not match a JWA standard curve id."; - - protected static ECParameterSpec getCurveByJwaId(String jwaCurveId) { - ECParameterSpec spec = null; - Curve curve = Curves.findById(jwaCurveId); - if (curve instanceof ECCurve) { - ECCurve ecCurve = (ECCurve) curve; - spec = ecCurve.toParameterSpec(); - } - if (spec == null) { - String msg = "Unrecognized JWA curve id '" + jwaCurveId + "'"; + protected static ECCurve getCurveByJwaId(String jwaCurveId) { + ECCurve curve = ECCurve.findById(jwaCurveId); + if (curve == null) { + String msg = "Unrecognized JWA EC curve id '" + jwaCurveId + "'"; throw new UnsupportedKeyException(msg); } - return spec; - } - - protected static String getJwaIdByCurve(EllipticCurve curve) { - ECCurve c = Curves.findBy(curve); - if (c == null) { - throw new UnsupportedKeyException(UNSUPPORTED_CURVE_MSG); - } - return c.getId(); + return curve; } /** @@ -85,146 +57,7 @@ static String toOctetString(int fieldSize, BigInteger coordinate) { return Encoders.BASE64URL.encode(bytes); } - /** - * Returns {@code true} if a given elliptic {@code curve} contains the specified {@code point}, {@code false} - * otherwise. Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) - * Weierstrass form: - *

- * y2 = x3 + ax + b - *

- * - * @param curve the Elliptic Curve to check - * @param point a point that may or may not be defined on the specified elliptic curve - * @return {@code true} if a given elliptic curve contains the specified {@code point}, {@code false} otherwise. - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - static boolean contains(EllipticCurve curve, ECPoint point) { - - if (ECPoint.POINT_INFINITY.equals(point)) { - return false; - } - - final BigInteger a = curve.getA(); - final BigInteger b = curve.getB(); - final BigInteger x = point.getAffineX(); - final BigInteger y = point.getAffineY(); - - // The reduced Weierstrass form y^2 = x^3 + ax + b reflects an elliptic curve E over any field K (e.g. all real - // numbers or all complex numbers, etc). For computational simplicity, cryptographic (e.g. NIST) elliptic curves - // restrict K to be a field of integers modulo a prime number 'p'. As such, we apply modulo p (the field prime) - // to the equation to account for the restricted field. For a nice overview of the math behind EC curves and - // their application in cryptography, see - // https://web.northeastern.edu/dummit/docs/cryptography_5_elliptic_curves_in_cryptography.pdf - - final BigInteger p = ((ECFieldFp) curve.getField()).getP(); - - // Verify the point coordinates are in field range: - if (x.compareTo(BigInteger.ZERO) < 0 || x.compareTo(p) >= 0 || - y.compareTo(BigInteger.ZERO) < 0 || y.compareTo(p) >= 0) { - return false; - } - - // Finally, assert Weierstrass form equality: - final BigInteger lhs = y.modPow(TWO, p); //mod p to account for field prime - final BigInteger rhs = x.modPow(THREE, p).add(a.multiply(x)).add(b).mod(p); //mod p to account for field prime - return lhs.equals(rhs); - } - - /** - * Multiply a point {@code p} by scalar {@code s} on the curve identified by {@code spec}. - * - * @param p the Elliptic Curve point to multiply - * @param s the scalar value to multiply - * @param spec the domain parameters that identify the Elliptic Curve containing point {@code p}. - */ - private static ECPoint multiply(ECPoint p, BigInteger s, ECParameterSpec spec) { - if (ECPoint.POINT_INFINITY.equals(p)) { - return p; - } - - EllipticCurve curve = spec.getCurve(); - BigInteger n = spec.getOrder(); - BigInteger k = s.mod(n); - - ECPoint r0 = ECPoint.POINT_INFINITY; - ECPoint r1 = p; - - // Montgomery Ladder implementation to mitigate side-channel attacks (i.e. an 'add' operation and a 'double' - // operation is calculated for every loop iteration, regardless if the 'add'' is needed or not) - // See: https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder - while (k.compareTo(BigInteger.ZERO) > 0) { - ECPoint temp = add(r0, r1, curve); - r0 = k.testBit(0) ? temp : r0; - r1 = doublePoint(r1, curve); - k = k.shiftRight(1); - } - - return r0; - } - - private static ECPoint add(ECPoint P, ECPoint Q, EllipticCurve curve) { - - if (ECPoint.POINT_INFINITY.equals(P)) { - return Q; - } else if (ECPoint.POINT_INFINITY.equals(Q)) { - return P; - } else if (P.equals(Q)) { - return doublePoint(P, curve); - } - - final BigInteger Px = P.getAffineX(); - final BigInteger Py = P.getAffineY(); - final BigInteger Qx = Q.getAffineX(); - final BigInteger Qy = Q.getAffineY(); - final BigInteger prime = ((ECFieldFp) curve.getField()).getP(); - final BigInteger slope = Qy.subtract(Py).multiply(Qx.subtract(Px).modInverse(prime)).mod(prime); - final BigInteger Rx = slope.pow(2).subtract(Px).subtract(Qx).mod(prime); - final BigInteger Ry = slope.multiply(Px.subtract(Rx)).subtract(Py).mod(prime); - - return new ECPoint(Rx, Ry); - } - - private static ECPoint doublePoint(ECPoint P, EllipticCurve curve) { - - if (ECPoint.POINT_INFINITY.equals(P)) { - return P; - } - - final BigInteger Px = P.getAffineX(); - final BigInteger Py = P.getAffineY(); - final BigInteger p = ((ECFieldFp) curve.getField()).getP(); - final BigInteger a = curve.getA(); - final BigInteger s = THREE.multiply(Px.pow(2)).add(a).mod(p).multiply(TWO.multiply(Py).modInverse(p)).mod(p); - final BigInteger x = s.pow(2).subtract(TWO.multiply(Px)).mod(p); - final BigInteger y = s.multiply(Px.subtract(x)).subtract(Py).mod(p); - - return new ECPoint(x, y); - } - AbstractEcJwkFactory(Class keyType, Set> fields) { super(DefaultEcPublicJwk.TYPE_VALUE, keyType, fields); } - - // visible for testing - protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { - return (ECPublicKey) keyFactory.generatePublic(spec); - } - - protected ECPublicKey derivePublic(final JwkContext ctx) { - final ECPrivateKey key = ctx.getKey(); - final ECParameterSpec params = key.getParams(); - final ECPoint w = multiply(params.getGenerator(), key.getS(), params); - final ECPublicKeySpec spec = new ECPublicKeySpec(w, params); - return generateKey(ctx, ECPublicKey.class, new CheckedFunction() { - @Override - public ECPublicKey apply(KeyFactory kf) { - try { - return derivePublic(kf, spec); - } catch (Exception e) { - String msg = "Unable to derive ECPublicKey from ECPrivateKey: " + e.getMessage(); - throw new UnsupportedKeyException(msg, e); - } - } - }); - } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java deleted file mode 100644 index 553da5b79..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2022 jsonwebtoken.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.impl.lang.DefaultRegistry; -import io.jsonwebtoken.impl.lang.Function; -import io.jsonwebtoken.impl.lang.IdRegistry; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.Registry; -import io.jsonwebtoken.security.Curve; - -import java.security.spec.EllipticCurve; -import java.util.Collection; -import java.util.LinkedHashSet; - -public final class Curves { - public static final Curve P_256 = new ECCurve("P-256", "secp256r1"); // JDK standard - public static final Curve P_384 = new ECCurve("P-384", "secp384r1"); // JDK standard - public static final Curve P_521 = new ECCurve("P-521", "secp521r1"); // JDK standard - - private static final Collection EC_CURVES = Collections.setOf((ECCurve) P_256, (ECCurve) P_384, (ECCurve) P_521); - - static final Collection VALUES = new LinkedHashSet<>(); - - static { - VALUES.addAll(EC_CURVES); - VALUES.addAll(EdwardsCurve.VALUES); - } - - private static final Registry CURVES_BY_ID = new IdRegistry<>("Elliptic Curve", VALUES); - private static final Registry CURVES_BY_JCA_NAME = new DefaultRegistry<>( - "Elliptic Curve", "JCA name", VALUES, new Function() { - @Override - public String apply(Curve curve) { - return ((DefaultCurve) curve).getJcaName(); - } - }); - - private static final Registry CURVES_BY_JCA_CURVE = new DefaultRegistry<>( - "Elliptic Curve", "ECCurve instance", EC_CURVES, new Function() { - @Override - public EllipticCurve apply(ECCurve curve) { - return curve.toParameterSpec().getCurve(); - } - }); - - //prevent instantiation - private Curves() { - } - - public static Curve findById(String jwaId) { - Assert.hasText(jwaId, "jwaId cannot be null or empty."); - return CURVES_BY_ID.get(jwaId); - } - - public static Curve findByJcaName(String jcaName) { - Assert.hasText(jcaName, "jcaName cannot be null or empty."); - return CURVES_BY_JCA_NAME.get(jcaName); - } - - public static ECCurve findBy(EllipticCurve curve) { - Assert.notNull(curve, "EllipticCurve argument cannot be null."); - return CURVES_BY_JCA_CURVE.get(curve); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ECCurve.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ECCurve.java index 2ad9db553..1d58e77e2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ECCurve.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ECCurve.java @@ -16,17 +16,104 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.KeyPairBuilder; +import io.jsonwebtoken.security.UnsupportedKeyException; +import java.math.BigInteger; import java.security.AlgorithmParameters; +import java.security.Key; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; import java.security.spec.ECGenParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; public class ECCurve extends DefaultCurve { + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger THREE = BigInteger.valueOf(3); + static final String KEY_PAIR_GENERATOR_JCA_NAME = "EC"; + public static final ECCurve P256 = new ECCurve("P-256", "secp256r1"); // JDK standard + public static final ECCurve P384 = new ECCurve("P-384", "secp384r1"); // JDK standard + public static final ECCurve P521 = new ECCurve("P-521", "secp521r1"); // JDK standard + + public static final Collection VALUES = Collections.setOf(P256, P384, P521); + private static final Map BY_ID = new LinkedHashMap<>(3); + private static final Map BY_JCA_CURVE = new LinkedHashMap<>(3); + + static { + for (ECCurve curve : VALUES) { + BY_ID.put(curve.getId(), curve); + } + for (ECCurve curve : VALUES) { + BY_JCA_CURVE.put(curve.spec.getCurve(), curve); + } + } + + static EllipticCurve assertJcaCurve(ECKey key) { + Assert.notNull(key, "ECKey cannot be null."); + ECParameterSpec spec = Assert.notNull(key.getParams(), "ECKey params() cannot be null."); + return Assert.notNull(spec.getCurve(), "ECKey params().getCurve() cannot be null."); + } + + static ECCurve findById(String id) { + return BY_ID.get(id); + } + + static ECCurve findByJcaCurve(EllipticCurve curve) { + return BY_JCA_CURVE.get(curve); + } + + static ECCurve findByKey(Key key) { + if (!(key instanceof ECKey)) { + return null; + } + ECKey ecKey = (ECKey) key; + ECParameterSpec spec = ecKey.getParams(); + if (spec == null) { + return null; + } + EllipticCurve jcaCurve = spec.getCurve(); + ECCurve curve = BY_JCA_CURVE.get(jcaCurve); + if (curve != null && key instanceof ECPublicKey) { + ECPublicKey pub = (ECPublicKey) key; + ECPoint w = pub.getW(); + if (w == null || !curve.contains(w)) { // don't support keys with a point not on its indicated curve + curve = null; + } + } + return curve; + } + + static ECCurve forKey(Key key) { + ECCurve curve = findByKey(key); + if (curve == null) { + String msg = "Unable to determine JWA-standard Elliptic Curve for specified key: " + + KeysBridge.toString(key); + throw new UnsupportedKeyException(msg); + } + return curve; + } + + static ECPublicKeySpec publicKeySpec(ECPrivateKey key) throws IllegalArgumentException { + EllipticCurve jcaCurve = assertJcaCurve(key); + ECCurve curve = BY_JCA_CURVE.get(jcaCurve); + Assert.notNull(curve, "There is no JWA-standard Elliptic Curve for specified ECPrivateKey."); + final ECPoint w = curve.multiply(key.getS()); + return new ECPublicKeySpec(w, curve.spec); + } + private final ECParameterSpec spec; public ECCurve(String id, String jcaName) { @@ -45,23 +132,161 @@ public ECParameterSpec toParameterSpec() { return this.spec; } + @Override + public KeyPairBuilder keyPair() { + return new DefaultKeyPairBuilder(KEY_PAIR_GENERATOR_JCA_NAME, toParameterSpec()); + } + + boolean contains(Key key) { + if (key instanceof ECPublicKey) { + ECPublicKey pub = (ECPublicKey) key; + ECParameterSpec pubSpec = pub.getParams(); + return pubSpec != null && + this.spec.getCurve().equals(pubSpec.getCurve()) && + contains(pub.getW()); + + } + return false; + } + + boolean contains(ECPoint point) { + return contains(this.spec.getCurve(), point); + } + /** - * Returns {@code true} if this elliptic curve contains the specified {@code point}, {@code false} - * otherwise. Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) + * Returns {@code true} if the specified curve contains the specified {@code point}, {@code false} otherwise. + * Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) * Weierstrass form: *

* y2 = x3 + ax + b *

* + * @param curve the EllipticCurve to check * @param point a point that may or may not be defined on this elliptic curve - * @return {@code true} if this elliptic curve contains the specified {@code point}, {@code false} otherwise. + * @return {@code true} if this curve contains the specified {@code point}, {@code false} otherwise. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + static boolean contains(EllipticCurve curve, ECPoint point) { + + if (point == null || ECPoint.POINT_INFINITY.equals(point)) { + return false; + } + + final BigInteger a = curve.getA(); + final BigInteger b = curve.getB(); + final BigInteger x = point.getAffineX(); + final BigInteger y = point.getAffineY(); + + // The reduced Weierstrass form y^2 = x^3 + ax + b reflects an elliptic curve E over any field K (e.g. all real + // numbers or all complex numbers, etc). For computational simplicity, cryptographic (e.g. NIST) elliptic curves + // restrict K to be a field of integers modulo a prime number 'p'. As such, we apply modulo p (the field prime) + // to the equation to account for the restricted field. For a nice overview of the math behind EC curves and + // their application in cryptography, see + // https://web.northeastern.edu/dummit/docs/cryptography_5_elliptic_curves_in_cryptography.pdf + + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); + + // Verify the point coordinates are in field range: + if (x.compareTo(BigInteger.ZERO) < 0 || x.compareTo(p) >= 0 || + y.compareTo(BigInteger.ZERO) < 0 || y.compareTo(p) >= 0) { + return false; + } + + // Finally, assert Weierstrass form equality: + final BigInteger lhs = y.modPow(TWO, p); //mod p to account for field prime + final BigInteger rhs = x.modPow(THREE, p).add(a.multiply(x)).add(b).mod(p); //mod p to account for field prime + return lhs.equals(rhs); + } + + /** + * Multiply this curve's generator (aka 'base point') by scalar {@code s} on the curve. + * + * @param s the scalar value to multiply */ - public boolean contains(ECPoint point) { - return AbstractEcJwkFactory.contains(spec.getCurve(), point); + private ECPoint multiply(BigInteger s) { + return multiply(this.spec.getGenerator(), s); } - @Override - public KeyPairBuilder keyPair() { - return new DefaultKeyPairBuilder(KEY_PAIR_GENERATOR_JCA_NAME, toParameterSpec()); + /** + * Multiply a point {@code p} by scalar {@code s} on the curve. + * + * @param p the Elliptic Curve point to multiply + * @param s the scalar value to multiply + */ + private ECPoint multiply(ECPoint p, BigInteger s) { + + if (ECPoint.POINT_INFINITY.equals(p)) { + return p; + } + + final BigInteger n = this.spec.getOrder(); + final BigInteger k = s.mod(n); + + ECPoint r0 = ECPoint.POINT_INFINITY; + ECPoint r1 = p; + + // Montgomery Ladder implementation to mitigate side-channel attacks (i.e. an 'add' operation and a 'double' + // operation is calculated for every loop iteration, regardless if the 'add'' is needed or not) + // See: https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder +// while (k.compareTo(BigInteger.ZERO) > 0) { +// ECPoint temp = add(r0, r1, curve); +// r0 = k.testBit(0) ? temp : r0; +// r1 = doublePoint(r1, curve); +// k = k.shiftRight(1); +// } + // above implementation (k.compareTo/k.shiftRight) works correctly , but this is a little faster: + for (int i = k.bitLength() - 1; i >= 0; i--) { + if (k.testBit(i)) { // bit == 1 + r0 = add(r0, r1); + r1 = doublePoint(r1); + } else { // bit == 0 + r1 = add(r0, r1); + r0 = doublePoint(r0); + } + } + + return r0; + } + + private ECPoint add(ECPoint P, ECPoint Q) { + + if (ECPoint.POINT_INFINITY.equals(P)) { + return Q; + } else if (ECPoint.POINT_INFINITY.equals(Q)) { + return P; + } else if (P.equals(Q)) { + return doublePoint(P); + } + + final EllipticCurve curve = this.spec.getCurve(); + + final BigInteger Px = P.getAffineX(); + final BigInteger Py = P.getAffineY(); + final BigInteger Qx = Q.getAffineX(); + final BigInteger Qy = Q.getAffineY(); + final BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + final BigInteger slope = Qy.subtract(Py).multiply(Qx.subtract(Px).modInverse(prime)).mod(prime); + final BigInteger Rx = slope.pow(2).subtract(Px).subtract(Qx).mod(prime); + final BigInteger Ry = slope.multiply(Px.subtract(Rx)).subtract(Py).mod(prime); + + return new ECPoint(Rx, Ry); + } + + private ECPoint doublePoint(ECPoint P) { + + if (ECPoint.POINT_INFINITY.equals(P)) { + return P; + } + + final EllipticCurve curve = this.spec.getCurve(); + final BigInteger Px = P.getAffineX(); + final BigInteger Py = P.getAffineY(); + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); + final BigInteger a = curve.getA(); + final BigInteger s = THREE.multiply(Px.pow(2)).add(a).mod(p).multiply(TWO.multiply(Py).modInverse(p)).mod(p); + final BigInteger x = s.pow(2).subtract(TWO.multiply(Px)).mod(p); + final BigInteger y = s.multiply(Px.subtract(x)).subtract(Py).mod(p); + + return new ECPoint(x, y); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java index 1c447b5c8..368c7d63a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -22,14 +22,16 @@ import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.EcPrivateJwk; import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.UnsupportedKeyException; import java.math.BigInteger; import java.security.KeyFactory; import java.security.PublicKey; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; -import java.security.spec.ECParameterSpec; import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; class EcPrivateJwkFactory extends AbstractEcJwkFactory { @@ -46,6 +48,27 @@ protected boolean supportsKeyValues(JwkContext ctx) { return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultEcPrivateJwk.D.getId()); } + // visible for testing + protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { + return (ECPublicKey) keyFactory.generatePublic(spec); + } + + protected ECPublicKey derivePublic(final JwkContext ctx) { + final ECPrivateKey key = ctx.getKey(); + return generateKey(ctx, ECPublicKey.class, new CheckedFunction() { + @Override + public ECPublicKey apply(KeyFactory kf) { + try { + ECPublicKeySpec spec = ECCurve.publicKeySpec(key); + return derivePublic(kf, spec); + } catch (Exception e) { + String msg = "Unable to derive ECPublicKey from ECPrivateKey: " + e.getMessage(); + throw new UnsupportedKeyException(msg, e); + } + } + }); + } + @Override protected EcPrivateJwk createJwkFromKey(JwkContext ctx) { @@ -93,9 +116,8 @@ protected EcPrivateJwk createJwkFromValues(final JwkContext ctx) { JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPublicJwk.FIELDS, ctx); EcPublicJwk pubJwk = EcPublicJwkFactory.INSTANCE.createJwk(pubCtx); - ECParameterSpec spec = getCurveByJwaId(curveId); - final ECPrivateKeySpec privateSpec = new ECPrivateKeySpec(d, spec); - + ECCurve curve = getCurveByJwaId(curveId); + final ECPrivateKeySpec privateSpec = new ECPrivateKeySpec(d, curve.toParameterSpec()); ECPrivateKey key = generateKey(ctx, new CheckedFunction() { @Override public ECPrivateKey apply(KeyFactory kf) throws Exception { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java index dd7c64981..4450d7f8e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -21,6 +21,7 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.EcPublicJwk; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.UnsupportedKeyException; import java.math.BigInteger; import java.security.KeyFactory; @@ -33,6 +34,8 @@ class EcPublicJwkFactory extends AbstractEcJwkFactory { + private static final String UNSUPPORTED_CURVE_MSG = "The specified ECKey curve does not match a JWA standard curve id."; + static final EcPublicJwkFactory INSTANCE = new EcPublicJwkFactory(); EcPublicJwkFactory() { @@ -54,6 +57,14 @@ protected static String jwkContainsErrorMessage(String curveId, Map j return String.format(fmt, curveId, jwk); } + protected static String getJwaIdByCurve(EllipticCurve curve) { + ECCurve c = ECCurve.findByJcaCurve(curve); + if (c == null) { + throw new UnsupportedKeyException(UNSUPPORTED_CURVE_MSG); + } + return c.getId(); + } + @Override protected EcPublicJwk createJwkFromKey(JwkContext ctx) { @@ -64,7 +75,7 @@ protected EcPublicJwk createJwkFromKey(JwkContext ctx) { ECPoint point = key.getW(); String curveId = getJwaIdByCurve(curve); - if (!contains(curve, point)) { + if (!ECCurve.contains(curve, point)) { String msg = keyContainsErrorMessage(curveId); throw new InvalidKeyException(msg); } @@ -89,16 +100,15 @@ protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { BigInteger x = reader.get(DefaultEcPublicJwk.X); BigInteger y = reader.get(DefaultEcPublicJwk.Y); - ECParameterSpec spec = getCurveByJwaId(curveId); + ECCurve curve = getCurveByJwaId(curveId); ECPoint point = new ECPoint(x, y); - if (!contains(spec.getCurve(), point)) { + if (!curve.contains(point)) { String msg = jwkContainsErrorMessage(curveId, ctx); throw new InvalidKeyException(msg); } - final ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, spec); - + final ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, curve.toParameterSpec()); ECPublicKey key = generateKey(ctx, new CheckedFunction() { @Override public ECPublicKey apply(KeyFactory kf) throws Exception { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java index 9a3452549..c62ff1288 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.KeyPairBuilder; @@ -44,8 +45,7 @@ final class EcSignatureAlgorithm extends AbstractSignatureAlgorithm { private static final String REQD_ORDER_BIT_LENGTH_MSG = "orderBitLength must equal 256, 384, or 521."; - private static final String DER_ENCODING_SYS_PROPERTY_NAME = - "io.jsonwebtoken.impl.crypto.EllipticCurveSignatureValidator.derEncodingSupported"; + private static final String DER_ENCODING_SYS_PROPERTY_NAME = "io.jsonwebtoken.impl.crypto.EllipticCurveSignatureValidator.derEncodingSupported"; private static final String ES256_OID = "1.2.840.10045.4.3.2"; private static final String ES384_OID = "1.2.840.10045.4.3.3"; @@ -104,13 +104,12 @@ private static boolean isSupportedOrderBitLength(int orderBitLength) { static final EcSignatureAlgorithm ES384 = new EcSignatureAlgorithm(384, ES384_OID); static final EcSignatureAlgorithm ES512 = new EcSignatureAlgorithm(521, ES512_OID); - private static final Map ALGS_BY_OID; + private static final Map BY_OID = new LinkedHashMap<>(3); static { - ALGS_BY_OID = new LinkedHashMap<>(3); - ALGS_BY_OID.put(ES256_OID, ES256); - ALGS_BY_OID.put(ES384_OID, ES384); - ALGS_BY_OID.put(ES512_OID, ES512); + for (EcSignatureAlgorithm alg : Collections.of(ES256, ES384, ES512)) { + BY_OID.put(alg.OID, alg); + } } static SignatureAlgorithm findByKey(Key key) { @@ -121,7 +120,7 @@ static SignatureAlgorithm findByKey(Key key) { } algName = algName.toUpperCase(Locale.ENGLISH); - SignatureAlgorithm alg = ALGS_BY_OID.get(algName); + SignatureAlgorithm alg = BY_OID.get(algName); if (alg != null) { return alg; } @@ -154,8 +153,7 @@ private EcSignatureAlgorithm(int orderBitLength, String oid) { @Override public KeyPairBuilder keyPair() { - return new DefaultKeyPairBuilder(ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, this.KEY_PAIR_GEN_PARAMS) - .random(Randoms.secureRandom()); + return new DefaultKeyPairBuilder(ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, this.KEY_PAIR_GEN_PARAMS).random(Randoms.secureRandom()); } @Override @@ -172,10 +170,7 @@ protected void validateKey(Key key, boolean signing) { int concatByteLength = sigFieldByteLength * 2; if (concatByteLength != this.signatureByteLength) { - String msg = "The provided Elliptic Curve " + keyType(signing) + " key's size (aka Order bit length) is " + - Bytes.bitsMsg(orderBitLength) + ", but the '" + name + "' algorithm requires EC Keys with " + - Bytes.bitsMsg(this.orderBitLength) + " per " + - "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; + String msg = "The provided Elliptic Curve " + keyType(signing) + " key's size (aka Order bit length) is " + Bytes.bitsMsg(orderBitLength) + ", but the '" + name + "' algorithm requires EC Keys with " + Bytes.bitsMsg(this.orderBitLength) + " per " + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; throw new InvalidKeyException(msg); } } @@ -229,9 +224,7 @@ public Boolean apply(Signature sig) { if (concatSignature[0] == 0x30 && "true".equalsIgnoreCase(System.getProperty(DER_ENCODING_SYS_PROPERTY_NAME))) { derSignature = concatSignature; } else { - String msg = "Provided signature is " + Bytes.bytesMsg(concatSignature.length) + " but " + - getId() + " signatures must be exactly " + Bytes.bytesMsg(signatureByteLength) + " per " + - "[RFC 7518, Section 3.4 (validation)](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; + String msg = "Provided signature is " + Bytes.bytesMsg(concatSignature.length) + " but " + getId() + " signatures must be exactly " + Bytes.bytesMsg(signatureByteLength) + " per " + "[RFC 7518, Section 3.4 (validation)](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; throw new SignatureException(msg); } } else { @@ -300,10 +293,7 @@ public static byte[] transcodeDERToConcat(final byte[] derSignature, int outputL int rawLen = Math.max(i, j); rawLen = Math.max(rawLen, outputLength / 2); - if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset - || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength - || derSignature[offset] != 2 - || derSignature[offset + 2 + rLength] != 2) { + if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength || derSignature[offset] != 2 || derSignature[offset + 2 + rLength] != 2) { throw new JwtException("Invalid ECDSA signature format"); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index 43db6eb80..5a908d995 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -24,6 +24,7 @@ import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.Curve; import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.DynamicJwkBuilder; import io.jsonwebtoken.security.EcPublicJwk; @@ -50,9 +51,6 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.interfaces.ECKey; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.ECParameterSpec; /** * @since JJWT_RELEASE_VERSION @@ -70,9 +68,9 @@ class EcdhKeyAlgorithm extends CryptoAlgorithm implements KeyAlgorithm WRAP_ALG; @@ -91,16 +89,8 @@ private static String idFor(KeyAlgorithm wrapAlg) { this.WRAP_ALG = Assert.notNull(wrapAlg, "Wrap algorithm cannot be null."); } - //visible for testing, for non-Edwards elliptic curves - protected KeyPair generateKeyPair(final Request request, final ECParameterSpec spec) { - Assert.notNull(spec, "request key params cannot be null."); - JcaTemplate template = new JcaTemplate(ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, request.getProvider(), - ensureSecureRandom(request)); - return template.generateKeyPair(spec); - } - //visible for testing, for Edwards elliptic curves - protected KeyPair generateKeyPair(SecureRandom random, EdwardsCurve curve, Provider provider) { + protected KeyPair generateKeyPair(Curve curve, Provider provider, SecureRandom random) { return curve.keyPair().provider(provider).random(random).build(); } @@ -170,8 +160,9 @@ private static EdwardsCurve assertAgreement(Key key, String exMsg) { EdwardsCurve curve; try { curve = EdwardsCurve.forKey(key); - } catch (Exception e) { - throw new UnsupportedKeyException(exMsg + " Cause: " + e.getMessage(), e); + } catch (Throwable t) { + String msg = exMsg + " Cause: " + t.getMessage(); + throw new UnsupportedKeyException(msg, t); } Assert.stateNotNull(curve, "EdwardsCurve instance cannot be null."); if (curve.isSignatureCurve()) { @@ -182,34 +173,38 @@ private static EdwardsCurve assertAgreement(Key key, String exMsg) { return curve; } + private static ECCurve assertEcCurve(Key key, String exMsg) { + ECCurve curve; + try { + curve = ECCurve.forKey(key); + } catch (Throwable t) { + String msg = exMsg + " Cause: " + t.getMessage(); + throw new UnsupportedKeyException(msg, t); + } + return Assert.stateNotNull(curve, "ECCurve instance cannot be null."); + } + @Override public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "Request cannot be null."); JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); PublicKey publicKey = Assert.notNull(request.getPayload(), "Encryption PublicKey cannot be null."); - KeyPair pair; // generated (ephemeral) key pair - final SecureRandom random = ensureSecureRandom(request); - DynamicJwkBuilder jwkBuilder = Jwks.builder().random(random).provider(request.getProvider()); - + Curve curve; if (publicKey instanceof ECKey) { - ECKey ecPublicKey = (ECKey) publicKey; - ECParameterSpec spec = Assert.notNull(ecPublicKey.getParams(), - "Encryption PublicKey params cannot be null."); + curve = assertEcCurve(publicKey, KEK_TYPE_MESSAGE); // note: we don't need to validate if specified key's point is on a supported curve here // because that will automatically be asserted when using Jwks.builder().... below - pair = generateKeyPair(request, spec); - // assert pair key types: - KeyPairs.getKey(pair, ECPublicKey.class); - KeyPairs.getKey(pair, ECPrivateKey.class); } else { // it must be an edwards curve key - EdwardsCurve curve = assertAgreement(publicKey, KEK_TYPE_MESSAGE); - Provider provider = request.getProvider(); - request = new DefaultKeyRequest<>(request.getPayload(), provider, random, - request.getHeader(), request.getEncryptionAlgorithm()); - pair = generateKeyPair(random, curve, provider); - jwkBuilder.provider(provider); + curve = assertAgreement(publicKey, KEK_TYPE_MESSAGE); } + Assert.stateNotNull(curve, "Internal implementation state: Curve cannot be null."); + + // Generate our ephemeral key pair: + final SecureRandom random = ensureSecureRandom(request); + final Provider provider = request.getProvider(); + DynamicJwkBuilder jwkBuilder = Jwks.builder().random(random).provider(provider); + KeyPair pair = generateKeyPair(curve, provider, random); Assert.stateNotNull(pair, "Internal implementation state: KeyPair cannot be null."); @@ -227,6 +222,15 @@ public KeyResult getEncryptionKey(KeyRequest request) throws Security return result; } + private T assertEpk(Class clazz, Object epk) { + if (!clazz.isInstance(epk)) { + String msg = "JWE Header " + DefaultJweHeader.EPK + " value is not a supported Elliptic Curve " + + "Public JWK. Value: " + epk; + throw new UnsupportedKeyException(msg); + } + return clazz.cast(epk); + } + @Override public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { @@ -237,27 +241,17 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest request) thro PublicJwk epk = reader.get(DefaultJweHeader.EPK); if (privateKey instanceof ECKey) { - ECKey ecPrivateKey = (ECKey) privateKey; - if (!(epk instanceof EcPublicJwk)) { - String msg = "JWE Header " + DefaultJweHeader.EPK + " value is not a supported Elliptic Curve " + - "Public JWK. Value: " + epk; - throw new UnsupportedKeyException(msg); - } - EcPublicJwk ecEpk = (EcPublicJwk) epk; + ECCurve curve = assertEcCurve(privateKey, KDK_TYPE_MESSAGE); + EcPublicJwk ecEpk = assertEpk(EcPublicJwk.class, epk); // While the EPK might be on a JWA-supported NIST curve, it must be on the private key's exact curve: - if (!EcPublicJwkFactory.contains(ecPrivateKey.getParams().getCurve(), ecEpk.toKey().getW())) { + if (!curve.contains(ecEpk.toKey())) { String msg = "JWE Header " + DefaultJweHeader.EPK + " value does not represent " + "a point on the expected curve."; throw new InvalidKeyException(msg); } } else { // it must be an Edwards Curve key EdwardsCurve privateKeyCurve = assertAgreement(privateKey, KDK_TYPE_MESSAGE); - if (!(epk instanceof OctetPublicJwk)) { - String msg = "JWE Header " + DefaultJweHeader.EPK + " value is not a supported Elliptic Curve " + - "Public JWK. Value: " + epk; - throw new UnsupportedKeyException(msg); - } - OctetPublicJwk oEpk = (OctetPublicJwk) epk; + OctetPublicJwk oEpk = assertEpk(OctetPublicJwk.class, epk); EdwardsCurve epkCurve = EdwardsCurve.forKey(oEpk.toKey()); if (!privateKeyCurve.equals(epkCurve)) { String msg = "JWE Header " + DefaultJweHeader.EPK + " value does not represent a point " + diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java index cb56da87b..7afabe22d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java @@ -17,11 +17,28 @@ import io.jsonwebtoken.impl.lang.DelegatingRegistry; import io.jsonwebtoken.impl.lang.IdRegistry; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.Curve; -@SuppressWarnings("unused") // used via reflection in io.jsonwebtoken.Jwks.CRV public final class StandardCurves extends DelegatingRegistry { + public StandardCurves() { - super(new IdRegistry<>("Elliptic Curve", Curves.VALUES, false)); + super(new IdRegistry<>("Elliptic Curve", Collections.of( + ECCurve.P256, + ECCurve.P384, + ECCurve.P521, + EdwardsCurve.X25519, + EdwardsCurve.X448, + EdwardsCurve.Ed25519, + EdwardsCurve.Ed448 + ), false)); } + +// public static Curve findByKey(Key key) { +// Curve curve = ECCurve.findByKey(key); +// if (curve == null) { +// curve = EdwardsCurve.findByKey(key); +// } +// return curve; +// } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwksCRVTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwksCRVTest.groovy index b423d5c3d..107e3151c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwksCRVTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwksCRVTest.groovy @@ -15,20 +15,27 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.impl.security.Curves +import io.jsonwebtoken.impl.security.ECCurve import io.jsonwebtoken.impl.security.EdwardsCurve +import io.jsonwebtoken.impl.security.StandardCurves import io.jsonwebtoken.security.Jwks import org.junit.Test import static org.junit.Assert.assertSame +import static org.junit.Assert.assertTrue class JwksCRVTest { + @Test + void testRegistry() { + assertTrue Jwks.CRV.get() instanceof StandardCurves + } + @Test void testInstances() { - assertSame Curves.P_256, Jwks.CRV.P256 - assertSame Curves.P_384, Jwks.CRV.P384 - assertSame Curves.P_521, Jwks.CRV.P521 + assertSame ECCurve.P256, Jwks.CRV.P256 + assertSame ECCurve.P384, Jwks.CRV.P384 + assertSame ECCurve.P521, Jwks.CRV.P521 assertSame EdwardsCurve.X25519, Jwks.CRV.X25519 assertSame EdwardsCurve.X448, Jwks.CRV.X448 assertSame EdwardsCurve.Ed25519, Jwks.CRV.Ed25519 diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy index 46a1833a6..78fb0a18b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy @@ -15,16 +15,10 @@ */ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.security.Jwk + import io.jsonwebtoken.security.UnsupportedKeyException import org.junit.Test -import java.security.KeyFactory -import java.security.interfaces.ECPrivateKey -import java.security.interfaces.ECPublicKey -import java.security.spec.* - import static org.junit.Assert.assertEquals import static org.junit.Assert.fail @@ -37,98 +31,8 @@ class AbstractEcJwkFactoryTest { AbstractEcJwkFactory.getCurveByJwaId(id) fail() } catch (UnsupportedKeyException e) { - String msg = "Unrecognized JWA curve id '$id'" + String msg = "Unrecognized JWA EC curve id '$id'" assertEquals msg, e.getMessage() } } - - @Test - void testUnsupportedCurve() { - def curve = new EllipticCurve(new TestECField(fieldSize: 1), BigInteger.ONE, BigInteger.TEN) - try { - AbstractEcJwkFactory.getJwaIdByCurve(curve) - fail() - } catch (UnsupportedKeyException e) { - assertEquals AbstractEcJwkFactory.UNSUPPORTED_CURVE_MSG, e.getMessage() - } - } - - @Test - void testMultiplyInfinity() { - ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') - def result = AbstractEcJwkFactory.multiply(ECPoint.POINT_INFINITY, BigInteger.valueOf(1), spec) - assertEquals ECPoint.POINT_INFINITY, result - } - - @Test - void testDoubleInfinity() { - ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') - def curve = spec.getCurve() - def result = AbstractEcJwkFactory.doublePoint(ECPoint.POINT_INFINITY, curve) - assertEquals ECPoint.POINT_INFINITY, result - } - - @Test - void testAddInfinity() { - ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') - def curve = spec.getCurve() - ECPoint point = new ECPoint(BigInteger.valueOf(1), BigInteger.valueOf(2)) // any point is fine for this test - def result = AbstractEcJwkFactory.add(ECPoint.POINT_INFINITY, point, curve) - //adding infinity to a point should return the point: - assertEquals point, result - //adding a point to infinity should return the point: - result = AbstractEcJwkFactory.add(point, ECPoint.POINT_INFINITY, curve) - assertEquals point, result - } - - @Test - void testAddSamePointDoublesIt() { - def pair = Jwts.SIG.ES256.keyPair().build() - def pub = pair.getPublic() as ECPublicKey - - def spec = pub.getParams() - def curve = spec.getCurve() - def point = pub.getW() - - def doubled = AbstractEcJwkFactory.doublePoint(point, curve) - def added = AbstractEcJwkFactory.add(point, point, curve) - assertEquals doubled, added - } - - @Test - void testDerivePublicFails() { - - def pair = Jwts.SIG.ES256.keyPair().build() - def priv = pair.getPrivate() as ECPrivateKey - - final def context = new DefaultJwkContext(DefaultEcPrivateJwk.FIELDS) - context.setKey(priv) - - def ex = new InvalidKeySpecException("invalid") - - def factory = new AbstractEcJwkFactory(ECPrivateKey.class, DefaultEcPrivateJwk.FIELDS) { - @Override - protected Jwk createJwkFromKey(JwkContext ctx) { - return null - } - - @Override - protected Jwk createJwkFromValues(JwkContext ctx) { - return null - } - - @Override - protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { - throw ex - } - } - - try { - factory.derivePublic(context) - fail() - } catch (UnsupportedKeyException expected) { - String msg = 'Unable to derive ECPublicKey from ECPrivateKey: invalid' - assertEquals msg, expected.getMessage() - } - } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ECCurveTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ECCurveTest.groovy index e38293ee7..5e45a2ccd 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ECCurveTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ECCurveTest.groovy @@ -15,19 +15,80 @@ */ package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.UnsupportedKeyException import org.junit.Test +import java.security.PublicKey import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec import java.security.spec.ECPoint +import java.security.spec.EllipticCurve -import static org.junit.Assert.assertFalse -import static org.junit.Assert.assertTrue +import static org.junit.Assert.* class ECCurveTest { + static void assertContains(ECCurve curve, PublicKey pub) { + assertTrue(curve.contains(pub)) + } + + @Test + void testContainsKeyTrue() { + assertContains(ECCurve.P256, TestKeys.ES256.pair.public) + assertContains(ECCurve.P384, TestKeys.ES384.pair.public) + assertContains(ECCurve.P521, TestKeys.ES512.pair.public) + } + + @Test + void testContainsKeyNull() { + ECCurve.VALUES.each { + assertFalse(it.contains(null)) + } + } + + @Test + void testContainsNonECPublicKey() { + ECCurve.VALUES.each { + assertFalse it.contains(TestKeys.HS256) + } + } + + @Test + void testContainsKeyNullParams() { + ECCurve.VALUES.each { + assertFalse it.contains(new TestECPublicKey()) + } + } + + @Test + void testContainsKeyNullJcaCurve() { + def src = TestKeys.ES256.pair.public.getParams() as ECParameterSpec + def spec = new ECParameterSpec(src.curve, src.generator, src.order, src.cofactor) { + @Override + EllipticCurve getCurve() { + return null + } + } + def key = new TestECKey(params: spec) + ECCurve.VALUES.each { + assertFalse it.contains(key) + } + } + + @Test + void testContainsKeyBadWPoint() { + ECCurve.VALUES.each { + def src = it.keyPair().build().public + def spec = src.getParams() as ECParameterSpec + def key = new TestECPublicKey(params: spec, w: new ECPoint(BigInteger.ONE, BigInteger.ONE)) + assertFalse it.contains(key) + } + } + @Test void testContainsTrue() { - ECCurve curve = (ECCurve) Curves.P_256 + ECCurve curve = ECCurve.P256 def pair = curve.keyPair().build() ECPublicKey ecPub = (ECPublicKey) pair.getPublic() assertTrue(curve.contains(ecPub.getW())) @@ -35,6 +96,109 @@ class ECCurveTest { @Test void testContainsFalse() { - assertFalse(((ECCurve) Curves.P_256).contains(new ECPoint(BigInteger.ONE, BigInteger.ONE))) + assertFalse(ECCurve.P256.contains(new ECPoint(BigInteger.ONE, BigInteger.ONE))) + } + + @Test + void testFindByJcaEllipticCurve() { + ECCurve.VALUES.each { + it.equals(ECCurve.findByJcaCurve(it.toParameterSpec().getCurve())) + } + } + + @Test + void testMultiplyInfinity() { + ECCurve.VALUES.each { + def result = it.multiply(ECPoint.POINT_INFINITY, BigInteger.valueOf(1)) + assertEquals ECPoint.POINT_INFINITY, result + + } + } + + @Test + void testDoubleInfinity() { + ECCurve.VALUES.each { + def result = it.doublePoint(ECPoint.POINT_INFINITY) + assertEquals ECPoint.POINT_INFINITY, result + } + } + + @Test + void testAddInfinity() { + ECCurve.VALUES.each { + def curve = it.spec.getCurve() + ECPoint point = new ECPoint(BigInteger.valueOf(1), BigInteger.valueOf(2)) // any point is fine for this test + def result = it.add(ECPoint.POINT_INFINITY, point) + //adding infinity to a point should return the point: + assertEquals point, result + //adding a point to infinity should return the point: + result = it.add(point, ECPoint.POINT_INFINITY) + assertEquals point, result + } + } + + @Test + void testAddSamePointDoublesIt() { + ECCurve.VALUES.each { + def pair = it.keyPair().build() + def pub = pair.getPublic() as ECPublicKey + def point = pub.getW() + def doubled = it.doublePoint(point) + def added = it.add(point, point) + assertEquals doubled, added + } + } + + @Test + void testFindByKeyNull() { + assertNull ECCurve.findByKey(null) + } + + @Test + void testFindByKeyNotECKey() { + assertNull ECCurve.findByKey(TestKeys.HS256) + } + + @Test + void testFindByKeyNullParams() { + assertNull ECCurve.findByKey(new TestECKey()) + } + + @Test + void testFindByKeyNullJcaCurve() { + def src = TestKeys.ES256.pair.public.getParams() as ECParameterSpec + def spec = new ECParameterSpec(src.curve, src.generator, src.order, src.cofactor) { + @Override + EllipticCurve getCurve() { + return null + } + } + assertNull ECCurve.findByKey(new TestECKey(params: spec)) + } + + @Test + void testFindByKeyWithNullWPoint() { + def spec = TestKeys.ES256.pair.public.getParams() as ECParameterSpec + assertNull ECCurve.findByKey(new TestECPublicKey(params: spec)) + } + + @Test + void testFindByKeyWithWPointNotOnCurve() { + def spec = TestKeys.ES256.pair.public.getParams() as ECParameterSpec + def key = new TestECPublicKey(params: spec, w: new ECPoint(BigInteger.ONE, BigInteger.ONE)) + assertNull ECCurve.findByKey(key) + } + + @Test + void testForKeyWithNonECPublicKey() { + def key = TestKeys.HS256 + try { + ECCurve.forKey(key) + fail() + } catch (UnsupportedKeyException expected) { + String msg = "Unable to determine JWA-standard Elliptic Curve for specified key: " + + "${KeysBridge.toString(key)}" + assertEquals msg, expected.getMessage() + } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy index 992149b06..97e75a437 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy @@ -15,9 +15,17 @@ */ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.MalformedKeyException +import io.jsonwebtoken.security.UnsupportedKeyException import org.junit.Test +import java.security.KeyFactory +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.ECPublicKeySpec +import java.security.spec.InvalidKeySpecException + import static org.junit.Assert.assertEquals import static org.junit.Assert.fail @@ -36,4 +44,31 @@ class EcPrivateJwkFactoryTest { assertEquals msg, expected.getMessage() } } + + @Test + void testDerivePublicFails() { + + def pair = Jwts.SIG.ES256.keyPair().build() + def priv = pair.getPrivate() as ECPrivateKey + + final def context = new DefaultJwkContext(DefaultEcPrivateJwk.FIELDS) + context.setKey(priv) + + def ex = new InvalidKeySpecException("invalid") + + def factory = new EcPrivateJwkFactory() { + @Override + protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { + throw ex + } + } + + try { + factory.derivePublic(context) + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'Unable to derive ECPublicKey from ECPrivateKey: invalid' + assertEquals msg, expected.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy index b3b235063..a0af274ef 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy @@ -18,8 +18,11 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.security.InvalidKeyException import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.MalformedKeyException +import io.jsonwebtoken.security.UnsupportedKeyException import org.junit.Test +import java.security.spec.EllipticCurve + import static org.junit.Assert.assertEquals import static org.junit.Assert.fail @@ -72,4 +75,15 @@ class EcPublicJwkFactoryTest { assertEquals msg, expected.getMessage() } } + + @Test + void testUnsupportedCurve() { + def curve = new EllipticCurve(new TestECField(fieldSize: 1), BigInteger.ONE, BigInteger.TEN) + try { + EcPublicJwkFactory.getJwaIdByCurve(curve) + fail() + } catch (UnsupportedKeyException e) { + assertEquals EcPublicJwkFactory.UNSUPPORTED_CURVE_MSG, e.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy index 278e3f616..66cad5968 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy @@ -62,7 +62,7 @@ class EcSignatureAlgorithmTest { @Test void testFindOidKeys() { - for(def alg : EcSignatureAlgorithm.ALGS_BY_OID.values()) { + for(def alg : EcSignatureAlgorithm.BY_OID.values()) { String name = "${alg.getId()}_OID" String oid = EcSignatureAlgorithm.metaClass.getAttribute(EcSignatureAlgorithm, name) as String assertEquals oid, alg.OID diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy index a4eb45427..45123164d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy @@ -124,9 +124,9 @@ class EcdhKeyAlgorithmTest { alg.getEncryptionKey(request) fail() } catch (UnsupportedKeyException expected) { - String msg = 'Key Encryption Key must be a java.security.interfaces.ECKey or valid Edwards Curve ' + - 'PublicKey instance. Cause: sun.security.rsa.RSAPublicKeyImpl with algorithm \'RSA\' is not a ' + - 'recognized Edwards Curve key.' + String msg = 'Key Encryption Key must be a java.security.interfaces.ECKey or Edwards Curve ' + + 'PublicKey on a supported curve. Cause: sun.security.rsa.RSAPublicKeyImpl with ' + + 'algorithm \'RSA\' is not a recognized Edwards Curve key.' assertEquals msg, expected.getMessage() } } @@ -142,9 +142,9 @@ class EcdhKeyAlgorithmTest { alg.getDecryptionKey(request) fail() } catch (UnsupportedKeyException expected) { - String msg = 'Key Decryption Key must be a java.security.interfaces.ECKey or valid Edwards Curve ' + - 'PrivateKey instance. Cause: sun.security.rsa.RSAPrivateCrtKeyImpl with algorithm \'RSA\' is ' + - 'not a recognized Edwards Curve key.' + String msg = 'Key Decryption Key must be a java.security.interfaces.ECKey or Edwards Curve ' + + 'PrivateKey on a supported curve. Cause: sun.security.rsa.RSAPrivateCrtKeyImpl with ' + + 'algorithm \'RSA\' is not a recognized Edwards Curve key.' assertEquals msg, expected.getMessage() } } @@ -221,4 +221,17 @@ class EcdhKeyAlgorithmTest { assertEquals msg, expected.getMessage() } } + + @Test + void testAssertEcCurveFails() { + def key = TestKeys.HS256 + try { + EcdhKeyAlgorithm.assertEcCurve(key, 'foo.') + fail() + } catch (UnsupportedKeyException expected) { + String msg = "foo. Cause: Unable to determine JWA-standard Elliptic Curve for specified " + + "key: ${KeysBridge.toString(key)}" + assertEquals msg, expected.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy index f312a5332..c7812ea68 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -26,7 +26,8 @@ import org.junit.Test import java.nio.charset.StandardCharsets import java.security.KeyPair -import java.security.spec.ECParameterSpec +import java.security.Provider +import java.security.SecureRandom import static org.junit.Assert.* @@ -98,7 +99,7 @@ class RFC7518AppendixCTest { //ensure keypair reflects required RFC test value: @Override - protected KeyPair generateKeyPair(Request request, ECParameterSpec spec) { + protected KeyPair generateKeyPair(Curve curve, Provider provider, SecureRandom random) { return aliceJwk.toKeyPair().toJavaKeyPair() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy index b68216af5..7e36d3407 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy @@ -30,8 +30,9 @@ import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets import java.security.KeyPair +import java.security.Provider +import java.security.SecureRandom import java.security.interfaces.RSAPublicKey -import java.security.spec.ECParameterSpec import static org.junit.Assert.assertEquals @@ -634,7 +635,7 @@ class RFC7520Section5Test { def RFC_EPK = Jwks.parser().build().parse(FIGURE_111) as EcPrivateJwk def alg = new EcdhKeyAlgorithm(wrapAlg) { @Override - protected KeyPair generateKeyPair(Request request, ECParameterSpec spec) { + protected KeyPair generateKeyPair(Curve curve, Provider provider, SecureRandom random) { return new KeyPair(RFC_EPK.toPublicJwk().toKey(), RFC_EPK.toKey()) // ensure RFC value } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC8037AppendixATest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC8037AppendixATest.groovy index 8a2773426..5be8ed106 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC8037AppendixATest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC8037AppendixATest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.Jwts import io.jsonwebtoken.impl.RfcTests +import io.jsonwebtoken.security.Curve import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.OctetPrivateJwk import io.jsonwebtoken.security.OctetPublicJwk @@ -154,7 +155,7 @@ class RFC8037AppendixATest { // ensure this is used during key algorithm execution per the RFC test case: def alg = new EcdhKeyAlgorithm(Jwts.KEY.A128KW) { @Override - protected KeyPair generateKeyPair(SecureRandom random, EdwardsCurve curve, Provider provider) { + protected KeyPair generateKeyPair(Curve curve, Provider provider, SecureRandom random) { return ephemJwk.toKeyPair().toJavaKeyPair() } } @@ -245,7 +246,7 @@ class RFC8037AppendixATest { // ensure this is used during key algorithm execution per the RFC test case: def alg = new EcdhKeyAlgorithm(Jwts.KEY.A256KW) { @Override - protected KeyPair generateKeyPair(SecureRandom random, EdwardsCurve curve, Provider provider) { + protected KeyPair generateKeyPair(Curve curve, Provider provider, SecureRandom random) { return ephemJwk.toKeyPair().toJavaKeyPair() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CurvesTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/StandardCurvesTest.groovy similarity index 76% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/CurvesTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/StandardCurvesTest.groovy index 76b1a8e79..196417e42 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CurvesTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/StandardCurvesTest.groovy @@ -15,42 +15,25 @@ */ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.security.Jwks import org.junit.Test -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertTrue +import static org.junit.Assert.* -class CurvesTest { +class StandardCurvesTest { - @Test - void testCtor() { - new Curves() // test coverage only - } + static final StandardCurves curves = (StandardCurves) Jwks.CRV.get() @Test void testFindById() { - Curves.VALUES.each { - it.equals(Curves.findById(it.getId())) - } - } - - @Test - void testFindByJcaName() { - Curves.VALUES.each { - it.equals(Curves.findByJcaName(it.getJcaName())) - } - } - - @Test - void testFindByEllipticCurve() { - Curves.EC_CURVES.each { - it.equals(Curves.findBy(it.toParameterSpec().getCurve())) + curves.values().each { + assertSame it, curves.get(it.getId()) } } @Test void testKeyPairBuilders() { - Curves.VALUES.each { + curves.values().each { def pair = it.keyPair().build() if (it instanceof ECCurve) { assertEquals ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, pair.getPublic().getAlgorithm()