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:
+ *
+ * - RSA: parameters may contain a {@code length} entry, defaults to 2048
+ * - EC: parameters may contain a {@code curve} entry, defaults to {@code "secp256r1"}. Curves must be given as std names.
+ * - EdDSA: parameters may contain a {@code curve} entry, defaults to {@code "Ed25519"}. Only supports Ed25519 and X25519
+ *
+ */
+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")