All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.edc.security.token.jwt.CryptoConverter Maven / Gradle / Ivy

There is a newer version: 0.10.1
Show newest version
/*
 *  Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
 *
 *  This program and the accompanying materials are made available under the
 *  terms of the Apache License, Version 2.0 which is available at
 *  https://www.apache.org/licenses/LICENSE-2.0
 *
 *  SPDX-License-Identifier: Apache-2.0
 *
 *  Contributors:
 *       Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
 *
 */

package org.eclipse.edc.security.token.jwt;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.Requirement;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.crypto.Ed25519Signer;
import com.nimbusds.jose.crypto.Ed25519Verifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.OctetKeyPair;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.util.Base64URL;
import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
import org.eclipse.edc.spi.EdcException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.EdECKey;
import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.EdECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EdECPoint;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import static java.lang.String.format;
import static java.util.Optional.ofNullable;

/**
 * Converter class that converts Java cryuptographic primitives (e.g. {@link PrivateKey}) into their Nimbus-counterparts needed to handle Json Web Tokens.
 *
 * @see Defined Algorithm Standard Names
 */
public class CryptoConverter {

    public static final String ALGORITHM_RSA = "rsa";
    public static final String ALGORITHM_EC = "ec";
    public static final String ALGORITHM_ECDSA = "ecdsa";
    public static final String ALGORITHM_EDDSA = "eddsa";
    public static final String ALGORITHM_ED25519 = "ed25519";
    public static final List SUPPORTED_ALGORITHMS = List.of(ALGORITHM_EC, ALGORITHM_RSA, ALGORITHM_EDDSA, ALGORITHM_ED25519);


    /**
     * Takes a Java {@link PrivateKey} object and creates a corresponding Nimbus {@link JWSSigner} for convenient use with JWTs.
     * Note that currently only the following key types are supported:
     * 
    *
  • RSA
  • *
  • EC: {@code key} argument is expected to be instanceof {@link ECPrivateKey}
  • *
  • EdDSA/Ed25519: {@code key} argument ist expected to be {@link EdECPrivateKey}. Both the Sun provider and the {@code org.bouncycastle.jce.provider.BouncyCastleProvider} are supported.
  • *
* * @param key the private key. * @return a {@link JWSSigner} * @throws IllegalArgumentException if the Curve of an EdDSA key is not "Ed25519" (x25519 and Ed448 are not supported!) * @throws IllegalArgumentException if the key is not in the list of supported algorithms ({@link CryptoConverter#SUPPORTED_ALGORITHMS}) * @throws EdcException if the {@link PrivateKey} is a EdDSA key and does not disclose its private bytes */ public static JWSSigner createSignerFor(PrivateKey key) { var algorithm = key.getAlgorithm().toLowerCase(); try { return switch (algorithm) { case ALGORITHM_EC, ALGORITHM_ECDSA -> getEcdsaSigner((ECPrivateKey) key); case ALGORITHM_RSA -> new RSASSASigner(key); case ALGORITHM_EDDSA, ALGORITHM_ED25519 -> createEdDsaVerifier(key); default -> throw new IllegalArgumentException(notSupportedError(algorithm)); }; } catch (JOSEException ex) { throw new EdcException(notSupportedError(algorithm), ex); } } @NotNull private static ECDSASigner getEcdsaSigner(ECPrivateKey key) throws JOSEException { var signer = new ECDSASigner(key); signer.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); return signer; } /** * Takes a Java {@link PublicKey} object and creates a corresponding Nimbus {@link JWSVerifier} for convenient use with JWTs. * Note that currently only the following key types are supported: *
    *
  • RSA
  • *
  • EC: {@code key} argument is expected to be instanceof {@link ECPrivateKey}
  • *
  • EdDSA/Ed25519: {@code key} argument ist expected to be {@link EdECPrivateKey}. Both the Sun provider and the {@code org.bouncycastle.jce.provider.BouncyCastleProvider} are supported.
  • *
* * @param publicKey the public key. * @return a {@link JWSSigner} * @throws IllegalArgumentException if the Curve of an EdDSA key is not "Ed25519" (x25519 and Ed448 are not supported!) * @throws IllegalArgumentException if the key is not in the list of supported algorithms ({@link CryptoConverter#SUPPORTED_ALGORITHMS}) * @throws EdcException if the {@link PublicKey} is a EdDSA key and does not disclose its private bytes */ public static JWSVerifier createVerifierFor(PublicKey publicKey) { var algorithm = publicKey.getAlgorithm().toLowerCase(); try { return switch (algorithm) { case ALGORITHM_EC, ALGORITHM_ECDSA -> getEcdsaVerifier((ECPublicKey) publicKey); case ALGORITHM_RSA -> new RSASSAVerifier((RSAPublicKey) publicKey); case ALGORITHM_EDDSA, ALGORITHM_ED25519 -> createEdDsaVerifier(publicKey); default -> throw new IllegalArgumentException(notSupportedError(algorithm)); }; } catch (JOSEException e) { throw new EdcException(notSupportedError(algorithm), e); } } /** * Converts a Java {@link KeyPair} into its JWK counterpart from Nimbus. Currently, only RSA, EC and EdDSA keys are supported, specifically: *
    *
  • EC: supports all named curves. If both private and public keys are specified, the conversion is straight forward. If only the public key is specified, then the * resulting JWK will not contain a private component (usually "d"). If only the private key is specified, then the public key is restored using elliptic curve multiplication. This is a fairly * costly operation, so it is avoided if possible.
  • *
  • EdDSA:
  • *
*

* Note that the "kid" parameter will be null, and the "key-use" will be set to {@link KeyUse#SIGNATURE}. If needed, the "kid" parameter can be set by * re-generating the resulting JWK using {@link JWK#toJSONObject()} and {@link JWK#parse(Map)}. * * @param keypair Must either contain the {@link PrivateKey}, the {@link PublicKey} or both. If neither is set, an {@link IllegalArgumentException} is thrown. * @return A Nimbus JWK. */ public static JWK createJwk(KeyPair keypair) { return createJwk(keypair, null); } /** * Converts a Java {@link KeyPair} into its JWK counterpart from Nimbus. Currently, only RSA, EC and EdDSA keys are supported, specifically: *

    *
  • EC: supports all named curves. If both private and public keys are specified, the conversion is straight forward. If only the public key is specified, then the * resulting JWK will not contain a private component (usually "d"). If only the private key is specified, then the public key is restored using elliptic curve multiplication. This is a fairly * costly operation, so it is avoided if possible.
  • *
  • EdDSA:
  • *
*

* Note that the "kid" parameter will be null, and the "key-use" will be set to {@link KeyUse#SIGNATURE}. If needed, the "kid" parameter can be set by * re-generating the resulting JWK using {@link JWK#toJSONObject()} and {@link JWK#parse(Map)}. * * @param keypair Must either contain the {@link PrivateKey}, the {@link PublicKey} or both. If neither is set, an {@link IllegalArgumentException} is thrown. * @param kid The key-ID that will be included in the JWK as 'kid' property. Can be null. * @return A Nimbus JWK. */ public static JWK createJwk(KeyPair keypair, @Nullable String kid) { if (keypair.getPrivate() == null && keypair.getPublic() == null) { throw new IllegalArgumentException("Invalid KeyPair: public and private key were both null!"); } var alg = ofNullable((Key) keypair.getPrivate()).orElse(keypair.getPublic()).getAlgorithm(); return switch (alg.toLowerCase()) { case ALGORITHM_EC -> convertEcKey(keypair, kid); case ALGORITHM_RSA -> convertRsaKey(keypair, kid); case ALGORITHM_EDDSA, ALGORITHM_ED25519 -> convertEdDsaKey(keypair, kid); default -> throw new IllegalArgumentException(notSupportedError(keypair.getPublic().getAlgorithm())); }; } /** * Attempts to determine the best suitable {@link JWSAlgorithm} for any given signer. Some signers support multiple, in * which case the first one marked RECOMMENDED is returned. If none is marked such, the first one is returned. * * @param signer the {@link JWSSigner} * @return the only {@link JWSAlgorithm}, or the one marked RECOMMENDED, or simply the first one. Returns null if no {@link JWSAlgorithm} can be determined. */ public static JWSAlgorithm getRecommendedAlgorithm(JWSSigner signer) { return getWithRequirement(signer, Requirement.REQUIRED) .orElseGet(() -> getWithRequirement(signer, Requirement.RECOMMENDED) .orElseGet(() -> getWithRequirement(signer, Requirement.OPTIONAL) .orElse(null))); } /** * Creates a {@link JWK} out of a map that represents a JSON structure. * * @param jsonObject The map containing the JSON * @return the corresponding key. * @throws RuntimeException if the JSON was malformed, or the JWK type was unknown. Typically, this wraps a {@link ParseException} */ public static JWK create(Map jsonObject) { if (jsonObject == null) return null; try { return JWK.parse(jsonObject); } catch (ParseException e) { throw new RuntimeException(e); } } /** * Creates a {@link JWK} out of a JSON string containing the key properties * * @param json The string containing plain JSON * @return the corresponding key. * @throws RuntimeException if the JSON was malformed, or the JWK type was unknown. Typically, this wraps a {@link ParseException} */ public static JWK create(String json) { if (json == null) return null; try { return JWK.parse(json); } catch (ParseException e) { throw new RuntimeException(e); } } /** * Creates a {@link JWSVerifier} from the base class {@link JWK}. Currently only supports EC, OKP and RSA keys. * * @param jwk The {@link JWK} for which the {@link JWSVerifier} is to be created. * @return the {@link JWSVerifier} * @throws UnsupportedOperationException if the verifier could not be created, in which case the root cause would be {@link JOSEException} */ public static JWSVerifier createVerifier(JWK jwk) { Objects.requireNonNull(jwk, "jwk cannot be null"); var value = jwk.getKeyType().getValue(); try { return switch (value) { case "EC" -> new ECDSAVerifier((ECKey) jwk); case "OKP" -> new Ed25519Verifier((OctetKeyPair) jwk); case "RSA" -> new RSASSAVerifier((RSAKey) jwk); default -> throw new UnsupportedOperationException(format("Cannot create JWSVerifier for JWK-type [%s], currently only supporting EC, OKP and RSA", value)); }; } catch (JOSEException ex) { throw new UnsupportedOperationException(ex); } } /** * Creates a {@link JWSSigner} from the base class {@link JWK}. Currently only supports EC, OKP and RSA keys. * * @param jwk The {@link JWK} for which the {@link JWSSigner} is to be created. * @return the {@link JWSSigner} * @throws UnsupportedOperationException if the signer could not be created, in which case the root cause would be {@link JOSEException} */ public static JWSSigner createSigner(JWK jwk) { var value = jwk.getKeyType().getValue(); try { return switch (value) { case "EC" -> new ECDSASigner((ECKey) jwk); case "OKP" -> new Ed25519Signer((OctetKeyPair) jwk); case "RSA" -> new RSASSASigner((RSAKey) jwk); default -> throw new UnsupportedOperationException(format("Cannot create JWSVerifier for JWK-type [%s], currently only supporting EC, OKP and RSA", value)); }; } catch (JOSEException ex) { throw new UnsupportedOperationException(ex); } } /** * Obtains the {@link Curve} from an EdDSA key, throwing an {@link IllegalArgumentException} if the curve was not in the * list of allowed algorithms/curves * * @param edKey either the private or public key * @param allowedCurves All curve names that are acceptable * @throws IllegalArgumentException if the key was not created on one of the accepted curves. */ private static Curve getCurveAllowing(EdECKey edKey, String... allowedCurves) { var curveName = edKey.getParams().getName(); if (!Arrays.asList(allowedCurves).contains(curveName.toLowerCase())) { throw new IllegalArgumentException("Unsupported curve: %s. Only the following curves is supported: %s.".formatted(curveName, String.join(",", allowedCurves))); } return Curve.parse(curveName); } private static RSAKey convertRsaKey(KeyPair keypair, @Nullable String kid) { if (keypair.getPublic() == null && keypair.getPrivate() == null) { throw new IllegalArgumentException("Either the public or the private key of a keypair must be non-null when converting RSA -> JWK"); } var key = Optional.ofNullable(keypair.getPublic()).orElseGet(() -> { var keySpec = new java.security.spec.RSAPublicKeySpec(((RSAPrivateCrtKey) keypair.getPrivate()).getModulus(), ((RSAPrivateCrtKey) keypair.getPrivate()).getPublicExponent()); try { var gen = KeyFactory.getInstance("RSA"); return gen.generatePublic(keySpec); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new RuntimeException(e); } }); var builder = new RSAKey.Builder((RSAPublicKey) key); if (keypair.getPrivate() != null) { builder.privateKey(keypair.getPrivate()); } return builder .keyID(kid) .keyUse(KeyUse.SIGNATURE).build(); } private static ECKey convertEcKey(KeyPair keypair, @Nullable String kid) { var pub = (ECPublicKey) keypair.getPublic(); var priv = (ECPrivateKey) keypair.getPrivate(); var key = ofNullable((java.security.interfaces.ECKey) pub).orElse(priv); // inspired by https://stackoverflow.com/a/70474128 try { var algorithmParameters = AlgorithmParameters.getInstance("EC"); algorithmParameters.init(key.getParams()); var curveName = algorithmParameters.getParameterSpec(ECGenParameterSpec.class).getName(); if (pub == null) { // we need to do elliptic curve multiplication, which Java does not natively support, at least there are no public interfaces. // thus we get bouncy with it. var bcSpec = EC5Util.convertSpec(priv.getParams()); var q = bcSpec.getG().multiply(priv.getS()).normalize(); // must be normalized var pointQjce = new ECPoint(q.getAffineXCoord().toBigInteger(), q.getAffineYCoord().toBigInteger()); var spec = new ECPublicKeySpec(pointQjce, priv.getParams()); pub = (ECPublicKey) KeyFactory.getInstance("EC").generatePublic(spec); } return new ECKey.Builder(Curve.forOID(curveName), pub).privateKey(priv).keyID(kid).keyUse(KeyUse.SIGNATURE).build(); } catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e) { throw new IllegalArgumentException(e); } } /** * reverses an array in-place */ private static byte[] reverseArray(byte[] array) { for (var i = 0; i < array.length / 2; i++) { var temp = array[i]; array[i] = array[array.length - 1 - i]; array[array.length - 1 - i] = temp; } return array; } private static Ed25519Verifier createEdDsaVerifier(PublicKey publicKey) throws JOSEException { var edKey = (EdECPublicKey) publicKey; var curve = getCurveAllowing(edKey, ALGORITHM_ED25519); var urlX = encodeX(edKey.getPoint()); var okp = new OctetKeyPair.Builder(curve, urlX) .build(); return new Ed25519Verifier(okp); } @NotNull private static ECDSAVerifier getEcdsaVerifier(ECPublicKey publicKey) throws JOSEException { var verifier = new ECDSAVerifier(publicKey); verifier.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); return verifier; } @NotNull private static Optional getWithRequirement(JWSSigner signer, Requirement requirement) { return signer.supportedJWSAlgorithms().stream() .filter(alg -> alg.getRequirement() == requirement) .findFirst(); } private static Ed25519Signer createEdDsaVerifier(PrivateKey key) throws JOSEException { var edKey = (EdECPrivateKey) key; var curve = getCurveAllowing(edKey, ALGORITHM_ED25519); var urlX = Base64URL.encode(new byte[0]); var urlD = encodeD(edKey); // technically, urlX should be the public bytes (i.e. public key), but we don't have that here, and we don't need it. // that is because internally, the Ed25519Signer only wraps the Ed25519Sign class from the Tink library, using only the private bytes ("d") var octetKeyPair = new OctetKeyPair.Builder(curve, urlX) .d(urlD) .build(); return new Ed25519Signer(octetKeyPair); } /** * Convert a KeyPair, that is expected to contain an EdDSA KeyPair, into the Nimbus type {@link OctetKeyPair}. Further, it is assumed that * either the private key, or the public key, or both are supplied. This method won't check that again. *

    *
  • If the private key and the public key are provided, the resulting JWK will contain a private component.
  • *
  • If only the public key is provided, the resulting JWK only contains the public parameters.
  • *
  • If only the private key is provided, the public key is restored from it.
  • *
*/ private static OctetKeyPair convertEdDsaKey(KeyPair keypair, @Nullable String kid) { var pub = (EdECPublicKey) keypair.getPublic(); var priv = (EdECPrivateKey) keypair.getPrivate(); // if the public key is not present, an empty byte array is set, because as with all elliptic curves the public // key can be recovered from the private key. OctetKeyPairs do this for us behind the scenes. var urlX = ofNullable(pub).map(pubkey -> encodeX(pubkey.getPoint())).orElseGet(() -> Base64URL.encode(new byte[0])); var urlD = ofNullable(priv).map(CryptoConverter::encodeD).orElse(null); var curveName = ofNullable((EdECKey) priv).orElse(pub).getParams().getName(); return new OctetKeyPair.Builder(Curve.parse(curveName), urlX) .d(urlD) .keyID(kid) .build(); } /** * Encodes the private key part of an EdDSA key as {@link Base64URL}, throws an exception if the binary representation can't be obtained */ @NotNull private static Base64URL encodeD(EdECPrivateKey edKey) { var bytes = edKey.getBytes().orElseThrow(() -> new EdcException("Private key is not willing to disclose its bytes")); return Base64URL.encode(bytes); } /** * Encodes the public key part of an EdDSA key as {@link Base64URL} */ @NotNull private static Base64URL encodeX(EdECPoint point) { var bytes = reverseArray(point.getY().toByteArray()); // when the X-coordinate of the curve is odd, we flip the highest-order bit of the first (or last, since we reversed) byte if (point.isXOdd()) { var mask = (byte) 128; // is 1000 0000 binary bytes[bytes.length - 1] ^= mask; // XOR means toggle the left-most bit } return Base64URL.encode(bytes); } private static String notSupportedError(String algorithm) { return "Could not convert PrivateKey to a JWSSigner, currently only the following types are supported: %s. The specified key was a %s" .formatted(String.join(",", SUPPORTED_ALGORITHMS), algorithm); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy