8000 Add explicit EC point-on-curve validation in ElytronEcdhEsAlgorithmProvider closes #53639 by atiqur-rahman-pro · Pull Request #53644 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.security.SecureRandom;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
Expand Down Expand Up @@ -170,14 +171,55 @@ private static PublicKey toPublicKey(ECPublicJWK jwk) {
BigInteger x = new BigInteger(1, Base64Url.decode(xStr));
BigInteger y = new BigInteger(1, Base64Url.decode(yStr));

ECParameterSpec spec = EllipticCurves.getSpec(crv);
if (spec == null) {
throw new IllegalArgumentException("Unsupported curve: " + crv);
}

validatePointOnCurve(x, y, spec);

Ec 8000 KeyUtil ecKeyUtil = new EcKeyUtil();
try {
return ecKeyUtil.publicKey(x, y, EllipticCurves.getSpec(crv));
return ecKeyUtil.publicKey(x, y, spec);
} catch (JoseException e) {
throw new IllegalArgumentException(e);
}
}

/**
* Validates that the point (x, y) lies on the given prime-field elliptic curve.
* Checks field membership (0 <= x, y < p) and the curve equation y² ≡ x³ + ax + b (mod p).
* All curves supported by jose4j (P-256, P-384, P-521, secp256k1) have prime fields
* and cofactor one, so this check is sufficient to reject invalid-curve attacks.
*/
private static void validatePointOnCurve(BigInteger x, BigInteger y, ECParameterSpec spec) {
if (!(spec.getCurve().getField() instanceof ECFieldFp)) {
throw new IllegalArgumentException("Only prime-field (Fp) curves are supported for EPK validation");
}
BigInteger p = ((ECFieldFp) spec.getCurve().getField()).getP();
BigInteger a = spec.getCurve().getA();
BigInteger b = spec.getCurve().getB();

if (x.signum() < 0 || x.compareTo(p) >= 0) {
throw new IllegalArgumentException("EPK x coordinate is not in the field");
}
if (y.signum() < 0 || y.compareTo(p) >= 0) {
throw new IllegalArgumentException("EPK y coordinate is not in the field");
Comment on lines +203 to +207
}

// y² mod p
BigInteger lhs = y.modPow(BigInteger.TWO, p);
// (x³ + ax + b) mod p
BigInteger rhs = x.modPow(BigInteger.valueOf(3), p)
.add(a.multiply(x))
.add(b)
.mod(p);

if (!lhs.equals(rhs)) {
throw new IllegalArgumentException("EPK point is not on the named curve");
}
}

private static byte[] deriveSharedSecret(Key publicKey, Key privateKey)
throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException {
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.ECFieldFp;
import java.security.spec.InvalidKeySpecException;

import org.keycloak.common.util.Base64Url;
Expand Down Expand Up @@ -90,6 +91,54 @@ public void encodeDecode()
Assert.assertArrayEquals(jweEncode.getContent(), jweDecode.getContent());
}

@Test
public void toPublicKeyRejectsOffCurvePoint() throws Exception {
// Valid P-256 x coordinate; y is off-curve (y + 1).
// Constructing a JWK with this point must be rejected before KeyAgreement.doPhase().
BigInteger y = new BigInteger(1, Base64Url.decode("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"))
.add(BigInteger.ONE);

assertRejectsEpkPoint("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", encodeCoordinate(y),
"not on the named curve");
}

@Test
public void toPublicKeyRejectsCoordinatesOutsideField() throws Exception {
BigInteger x = new BigInteger(1, Base64Url.decode("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ"));
BigInteger y = new BigInteger(1, Base64Url.decode("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"));
BigInteger p = ((ECFieldFp) EllipticCurves.getSpec("P-256").getCurve().getField()).getP();

assertRejectsEpkPoint(encodeCoordinate(x.add(p)), encodeCoordinate(y),
"x coordinate is not in the field");
assertRejectsEpkPoint("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", encodeCoordinate(y.add(p)),
"y coordinate is not in the field");
}

private void assertRejectsEpkPoint(String x, String y, String expectedMessage) throws Exception {
org.keycloak.jose.jwk.ECPublicJWK jwk = new org.keycloak.jose.jwk.ECPublicJWK();
jwk.setCrv("P-256");
jwk.setX(x);
jwk.setY(y);

JWEHeader header = JWEHeader.builder()
.algorithm(Algorithm.ECDH_ES_A128KW)
.encryptionAlgorithm(JWEConstants.A128CBC_HS256)
.ephemeralPublicKey(jwk)
.build();
PrivateKey decryptionKey = getPrivateKey("P-256", "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw");
IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class,
() -> new ElytronEcdhEsAlgorithmProvider().decodeCek(new byte[0], decryptionKey, header, null));
Assert.assertTrue(e.getMessage().contains(expectedMessage));
}

private String encodeCoordinate(BigInteger value) {
byte[] bytes = value.toByteArray();
if (bytes.length > 1 && bytes[0] == 0) {
bytes = java.util.Arrays.copyOfRange(bytes, 1, bytes.length);
}
return Base64Url.encode(bytes);
}

private PublicKey getPublicKey(String crv, String xStr, String yStr) throws JoseException {
BigInteger x = new BigInteger(1, Base64Url.decode(xStr));
BigInteger y = new BigInteger(1, Base64Url.decode(yStr));
Expand All @@ -102,4 +151,4 @@ private PrivateKey getPrivateKey(String crv, String dStr) throws JoseException {
EcKeyUtil ecKeyUtil = new EcKeyUtil();
return ecKeyUtil.privateKey(d, EllipticCurves.getSpec(crv));
}
}
}
Loading
0