Skip to content

Commit

Permalink
feat: create KeyPairGenerator (#222)
Browse files Browse the repository at this point in the history
* feat: implement KeyPairGenerator

* pr remarks
  • Loading branch information
paullatzelsperger authored Jan 16, 2024
1 parent 3ad06c8 commit 2a53606
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 0 deletions.
9 changes: 9 additions & 0 deletions extensions/common/security/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
`java-library`
}

dependencies {
implementation(libs.edc.spi.core)
implementation(libs.edc.util)
testImplementation(libs.edc.junit)
}
Original file line number Diff line number Diff line change
@@ -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:
* <ul>
* <li>RSA: parameters may contain a {@code length} entry, defaults to 2048</li>
* <li>EC: parameters may contain a {@code curve} entry, defaults to {@code "secp256r1"}. Curves must be given as std names.</li>
* <li>EdDSA: parameters may contain a {@code curve} entry, defaults to {@code "Ed25519"}. Only supports Ed25519 and X25519</li>
* </ul>
*/
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<String> SUPPORTED_ALGORITHMS = List.of(ALGORITHM_EC, ALGORITHM_RSA, ALGORITHM_EDDSA);
public static final List<String> 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<KeyPair> generateKeyPair(String algorithm, Map<String, Object> 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<KeyPair> 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<KeyPair> 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<KeyPair> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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: .*.");
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 2a53606

Please sign in to comment.