From 2a5360639b281cac1f4cc893767d5692e1cde205 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:26:27 +0100 Subject: [PATCH] feat: create KeyPairGenerator (#222) * feat: implement KeyPairGenerator * pr remarks --- extensions/common/security/build.gradle.kts | 9 ++ .../security/KeyPairGenerator.java | 114 ++++++++++++++ .../security/KeyPairGeneratorTest.java | 139 ++++++++++++++++++ settings.gradle.kts | 1 + 4 files changed, 263 insertions(+) create mode 100644 extensions/common/security/build.gradle.kts create mode 100644 extensions/common/security/src/main/java/org/eclipse/edc/identityhub/security/KeyPairGenerator.java create mode 100644 extensions/common/security/src/test/java/org/eclipse/edc/identityhub/security/KeyPairGeneratorTest.java diff --git a/extensions/common/security/build.gradle.kts b/extensions/common/security/build.gradle.kts new file mode 100644 index 000000000..fa517fa65 --- /dev/null +++ b/extensions/common/security/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + `java-library` +} + +dependencies { + implementation(libs.edc.spi.core) + implementation(libs.edc.util) + testImplementation(libs.edc.junit) +} diff --git a/extensions/common/security/src/main/java/org/eclipse/edc/identityhub/security/KeyPairGenerator.java b/extensions/common/security/src/main/java/org/eclipse/edc/identityhub/security/KeyPairGenerator.java new file mode 100644 index 000000000..11767fb5e --- /dev/null +++ b/extensions/common/security/src/main/java/org/eclipse/edc/identityhub/security/KeyPairGenerator.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.security; + +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.util.string.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; +import java.util.List; +import java.util.Map; + +/** + * Convenience class, that takes an algorithm name and generator parameters and creates a {@link KeyPair}. + * Supports the following algorithms: + * + */ +public class KeyPairGenerator { + + public static final String ALGORITHM_RSA = "RSA"; + public static final String ALGORITHM_EC = "EC"; + public static final String ALGORITHM_EDDSA = "EDDSA"; + public static final String CURVE_ED25519 = "ed25519"; + public static final String CURVE_X25519 = "x25519"; + public static final List SUPPORTED_ALGORITHMS = List.of(ALGORITHM_EC, ALGORITHM_RSA, ALGORITHM_EDDSA); + public static final List SUPPORTED_EDDSA_CURVES = List.of(CURVE_ED25519, CURVE_X25519); + public static final int RSA_DEFAULT_LENGTH = 2048; + private static final String RSA_PARAM_LENGTH = "length"; + private static final String EC_PARAM_CURVE = "curve"; + private static final String EC_DEFAULT_CURVE = "secp256r1"; + + /** + * Generate a Java {@link KeyPair} from an algorithm identifier and generator parameters. + * + * @param algorithm One of [EC, RSA, EdDSA]. If null, defaults to "EdDSA". + * @param parameters May contain specific paramters, such as "length" (RSA), or a "curve" (EC and EdDSA). May be empty, not null. + * @return A {@link KeyPair}, or a failure indicating what went wrong. + */ + public static Result generateKeyPair(String algorithm, Map parameters) { + if (StringUtils.isNullOrBlank(algorithm)) { + return generateEdDsa(CURVE_ED25519); + } + algorithm = algorithm.toUpperCase(); + if (SUPPORTED_ALGORITHMS.contains(algorithm)) { + return switch (algorithm) { + case ALGORITHM_RSA -> generateRsa(Integer.parseInt(parameters.getOrDefault(RSA_PARAM_LENGTH, RSA_DEFAULT_LENGTH).toString())); + case ALGORITHM_EC -> generateEc(parameters.getOrDefault(EC_PARAM_CURVE, EC_DEFAULT_CURVE).toString()); + case ALGORITHM_EDDSA -> generateEdDsa(parameters.getOrDefault(EC_PARAM_CURVE, CURVE_ED25519).toString()); + default -> Result.failure(notSupportedError(algorithm)); + }; + } + return Result.failure(notSupportedError(algorithm)); + } + + private static Result generateEc(String stdName) { + try { + var javaGenerator = java.security.KeyPairGenerator.getInstance(ALGORITHM_EC); + javaGenerator.initialize(new ECGenParameterSpec(stdName)); + return Result.success(javaGenerator.generateKeyPair()); + } catch (NoSuchAlgorithmException e) { + return Result.failure("Error generating EC keys: " + e); + } catch (InvalidAlgorithmParameterException e) { + return Result.failure("Error generating EC keys: %s is not a valid or supported EC curve std name. Details: %s".formatted(stdName, e.getMessage())); + } + } + + private static Result generateEdDsa(@NotNull String curve) { + curve = curve.toLowerCase(); + if (SUPPORTED_EDDSA_CURVES.contains(curve)) { + try { + var javaGenerator = java.security.KeyPairGenerator.getInstance(curve); + return Result.success(javaGenerator.generateKeyPair()); + } catch (NoSuchAlgorithmException e) { + return Result.failure("Error generating EdDSA/Ed25519 keys: " + e); + } + } + return Result.failure("Unsupported EdDSA Curve: %s. Currently only these are supported: %s.".formatted(curve, String.join(",", SUPPORTED_EDDSA_CURVES))); + } + + private static Result generateRsa(int length) { + try { + var javaGenerator = java.security.KeyPairGenerator.getInstance(ALGORITHM_RSA); + javaGenerator.initialize(length, new SecureRandom()); + return Result.success(javaGenerator.generateKeyPair()); + } catch (NoSuchAlgorithmException e) { + return Result.failure("Error generating RSA keys: " + e); + } + } + + private static String notSupportedError(String algorithm) { + return "Could not generate key pair for algorithm '%s'. Currently only the following algorithms are supported: %s." + .formatted(algorithm, String.join(",", SUPPORTED_ALGORITHMS)); + } +} diff --git a/extensions/common/security/src/test/java/org/eclipse/edc/identityhub/security/KeyPairGeneratorTest.java b/extensions/common/security/src/test/java/org/eclipse/edc/identityhub/security/KeyPairGeneratorTest.java new file mode 100644 index 000000000..beb2a94d5 --- /dev/null +++ b/extensions/common/security/src/test/java/org/eclipse/edc/identityhub/security/KeyPairGeneratorTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.security; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.security.AlgorithmParameters; +import java.security.InvalidParameterException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Map; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +class KeyPairGeneratorTest { + + @Test + void generateKeyPair_rsa_defaultLength() { + var rsaResult = KeyPairGenerator.generateKeyPair("RSA", Map.of()); + assertThat(rsaResult).isSucceeded() + .extracting(KeyPair::getPrivate) + .isInstanceOf(RSAPrivateKey.class); + + var key = (RSAPrivateKey) rsaResult.getContent().getPrivate(); + Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(2048); //could theoretically be less if key has 8 leading zeros, but we control the key generation. + } + + @Test + void generateKeyPair_rsa_withLength() { + var rsaResult = KeyPairGenerator.generateKeyPair("RSA", Map.of("length", 4096)); + assertThat(rsaResult).isSucceeded() + .extracting(KeyPair::getPrivate) + .isInstanceOf(RSAPrivateKey.class); + + var key = (RSAPrivateKey) rsaResult.getContent().getPrivate(); + Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(4096); //could theoretically be less if key has 8 leading zeros, but we control the key generation. + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, -1, Integer.MAX_VALUE}) + void generateKeyPair_rsa_withInvalidLength(int invalidLength) { + Assertions.assertThatThrownBy(() -> KeyPairGenerator.generateKeyPair("RSA", Map.of("length", invalidLength))).isInstanceOf(InvalidParameterException.class); + } + + @Test + void generateKeyPair_ec_defaultCurve() throws InvalidParameterSpecException, NoSuchAlgorithmException { + var ecResult = KeyPairGenerator.generateKeyPair("EC", Map.of()); + assertThat(ecResult).isSucceeded() + .extracting(KeyPair::getPrivate) + .isInstanceOf(ECPrivateKey.class); + + var key = (ECPrivateKey) ecResult.getContent().getPrivate(); + var algorithmParameters = AlgorithmParameters.getInstance("EC"); + algorithmParameters.init(key.getParams()); + var oid = algorithmParameters.getParameterSpec(ECGenParameterSpec.class).getName(); + Assertions.assertThat(oid).isEqualTo("1.2.840.10045.3.1.7"); // no easy way to get the std name, only the OID + } + + @ParameterizedTest() + @ValueSource(strings = {"secp256r1", "secp384r1", "secp521r1", "SECP256R1", "SecP521R1"}) + void generateKeyPair_ec_withCurve(String curve) { + var ecResult = KeyPairGenerator.generateKeyPair("EC", Map.of("curve", curve)); + assertThat(ecResult).isSucceeded() + .extracting(KeyPair::getPrivate) + .isInstanceOf(ECPrivateKey.class); + } + + @ParameterizedTest() + @ValueSource(strings = {"secp256k1", "foobar"}) + @EmptySource + void generateKeyPair_ec_withInvalidCurve(String curve) { + var ecResult = KeyPairGenerator.generateKeyPair("EC", Map.of("curve", curve)); + assertThat(ecResult).isFailed() + .detail().contains("not a valid or supported EC curve std name"); + } + + + @Test + void generateKeyPair_edDsa() { + var edDsaResult = KeyPairGenerator.generateKeyPair("EdDSA", Map.of()); + assertThat(edDsaResult).isSucceeded() + .extracting(KeyPair::getPrivate) + .satisfies(k -> Assertions.assertThat(k.getClass().getName()).isEqualTo("sun.security.ec.ed.EdDSAPrivateKeyImpl")); // not available at compile time + } + + @ParameterizedTest + @ValueSource(strings = {"Ed25519", "X25519", "ed25519", "x25519", "ED25519"}) + void generateKeyPair_edDsa_withValidCurve(String curve) { + var edDsaResult = KeyPairGenerator.generateKeyPair("EdDSA", Map.of("curve", curve)); + assertThat(edDsaResult).isSucceeded() + .extracting(KeyPair::getPrivate) + .satisfies(k -> Assertions.assertThat(k.getClass().getName()).startsWith("sun.security.ec.")); // not available at compile time + } + + @ParameterizedTest + @ValueSource(strings = {"Ed448", "x448", "foobar"}) + void generateKeyPair_edDsa_withInvalidCurve(String invalidCurve) { + var edDsaResult = KeyPairGenerator.generateKeyPair("EdDSA", Map.of("curve", invalidCurve)); + assertThat(edDsaResult).isFailed() + .detail().contains("Unsupported EdDSA Curve: %s. Currently only these are supported: ed25519,x25519.".formatted(invalidCurve.toLowerCase())); + } + + @ParameterizedTest + @NullSource + @EmptySource + void generateKeyPair_noAlgorithm(String nullOrEmpty) { + var result = KeyPairGenerator.generateKeyPair(nullOrEmpty, Map.of("foo", "bar")); + assertThat(result).isSucceeded() + .extracting(KeyPair::getPrivate) + .satisfies(k -> Assertions.assertThat(k.getClass().getName()).isEqualTo("sun.security.ec.ed.EdDSAPrivateKeyImpl")); // not available at compile time + } + + @Test + void generateKeyPair_unknownAlgorithm() { + assertThat(KeyPairGenerator.generateKeyPair("foobar", Map.of())).isFailed() + .detail().matches("Could not generate key pair for algorithm '.*'. Currently only the following algorithms are supported: .*."); + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b6a401ffb..a2c3f5adf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include(":core:identity-hub-credentials") include(":core:identity-hub-did") // extension modules +include(":extensions:common:security") include(":extensions:store:sql:identity-hub-did-store-sql") include(":extensions:store:sql:identity-hub-credentials-store-sql") include(":extensions:did:local-did-publisher")