diff --git a/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/jwt/Issuer.java b/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/jwt/Issuer.java index 27528a51169..3648b27e853 100644 --- a/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/jwt/Issuer.java +++ b/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/jwt/Issuer.java @@ -31,8 +31,14 @@ import java.security.PublicKey; import java.security.spec.KeySpec; import java.security.spec.RSAPublicKeySpec; -import java.time.Duration; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECPoint; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.ECGenParameterSpec; +import java.security.AlgorithmParameters; import java.util.Base64; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -214,6 +220,8 @@ private PublicKey buildPublicKey(JsonNode details) throws BadKeyDescriptionExcep switch (kty) { case "RSA": return buildRSAPublicKey(details); + case "EC": + return buildECPublicKey(details); default: throw new BadKeyDescriptionException("Unknown key type " + kty); } @@ -249,6 +257,41 @@ private PublicKey buildRSAPublicKey(JsonNode details) throws BadKeyDescriptionEx } } + private ECParameterSpec getECParameterSpec(String crv) throws GeneralSecurityException { + String name; + + switch (crv) { + case "P-256": + name = "secp256r1"; + break; + case "P-384": + name = "secp384r1"; + break; + case "P-521": + name = "secp521r1"; + break; + default: + throw new GeneralSecurityException("Unsupported curve: " + crv); + } + AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); + parameters.init(new ECGenParameterSpec(name)); + return parameters.getParameterSpec(ECParameterSpec.class); + } + + private PublicKey buildECPublicKey(JsonNode details) throws BadKeyDescriptionException { + try { + String crv = getString(details, "crv"); + byte[] x = Base64.getUrlDecoder().decode(getString(details, "x")); + byte[] y = Base64.getUrlDecoder().decode(getString(details, "y")); + ECParameterSpec params = getECParameterSpec(crv); + ECPoint point = new ECPoint(new BigInteger(1, x), new BigInteger(1, y)); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, params); + return KeyFactory.getInstance("EC").generatePublic(pubSpec); + } catch (GeneralSecurityException e) { + throw new BadKeyDescriptionException("Unable to build EC public key: " + e.toString()); + } + } + public void checkIssued(JsonWebToken token) throws AuthenticationException { Map> keyMap = keys.get() .orElseThrow(msg -> new AuthenticationException( diff --git a/modules/gplazma2/src/main/java/org/dcache/gplazma/util/JsonWebToken.java b/modules/gplazma2/src/main/java/org/dcache/gplazma/util/JsonWebToken.java index f7b12f52f83..6997b9c5fac 100644 --- a/modules/gplazma2/src/main/java/org/dcache/gplazma/util/JsonWebToken.java +++ b/modules/gplazma2/src/main/java/org/dcache/gplazma/util/JsonWebToken.java @@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; import java.security.PublicKey; import java.security.Signature; import java.time.Instant; @@ -113,12 +114,57 @@ public String getKeyIdentifier() { return kid; } + private static byte[] transcodeJWTECDSASignatureToDER(byte[] jwsSignature) throws SignatureException { + int rawLen = jwsSignature.length / 2; + + // Find the start of R (skip leading zeros) + int rStart = 0; + while (rStart < rawLen && jwsSignature[rStart] == 0) { + rStart++; + } + int rLen = rawLen - rStart; + + // Find the start of S (skip leading zeros) + int sStart = rawLen; + while (sStart < jwsSignature.length && jwsSignature[sStart] == 0) { + sStart++; + } + int sLen = rawLen - (sStart - rawLen); + + int totalLen = 2 + 2 + rLen + 2 + sLen; // SEQUENCE + INTEGER(R) + INTEGER(S) + int offset = 0; + byte[] der = new byte[totalLen]; + + der[offset++] = 0x30; // SEQUENCE + der[offset++] = (byte) (totalLen - 2); + + // INTEGER R + der[offset++] = 0x02; + der[offset++] = (byte) rLen; + System.arraycopy(jwsSignature, rawLen - rLen, der, offset, rLen); + offset += rLen; + + // INTEGER S + der[offset++] = 0x02; + der[offset++] = (byte) sLen; + System.arraycopy(jwsSignature, jwsSignature.length - sLen, der, offset, sLen); + + return der; + } + public boolean isSignedBy(PublicKey key) { try { Signature signature = getSignature(); signature.initVerify(key); signature.update(unsignedToken); - return signature.verify(this.signature); + byte[] sig = this.signature; + if (alg.startsWith("ES")) { + sig = transcodeJWTECDSASignatureToDER(sig); + } + return signature.verify(sig); + } catch (SignatureException e) { + LOGGER.warn("Problem verifying signature: {}", e.toString()); + return false; } catch (GeneralSecurityException e) { LOGGER.warn("Problem verifying signature: {}", e.toString()); return false; @@ -129,6 +175,12 @@ private Signature getSignature() throws GeneralSecurityException { switch (alg) { case "RS256": return Signature.getInstance("SHA256withRSA"); + case "ES256": + return Signature.getInstance("SHA256withECDSA"); + case "ES384": + return Signature.getInstance("SHA384withECDSA"); + case "ES512": + return Signature.getInstance("SHA512withECDSA"); default: throw new NoSuchAlgorithmException("Unknown JWT alg " + alg); }