diff --git a/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/KeyParserRegistryImpl.java b/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/KeyParserRegistryImpl.java index 99af84c6b31..8daafc36fe9 100644 --- a/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/KeyParserRegistryImpl.java +++ b/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/KeyParserRegistryImpl.java @@ -37,4 +37,12 @@ public Result parse(String encoded) { .map(kp -> kp.parse(encoded)) .orElseGet(() -> Result.failure("No parser found that can handle that format.")); } + + @Override + public Result parsePublic(String encoded) { + return parsers.stream().filter(kp -> kp.canHandle(encoded)) + .findFirst() + .map(kp -> kp.parsePublic(encoded)) + .orElseGet(() -> Result.failure("No parser found that can handle that format.")); + } } diff --git a/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/JwkParser.java b/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/JwkParser.java index f87e78f149b..b86852a1839 100644 --- a/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/JwkParser.java +++ b/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/JwkParser.java @@ -86,8 +86,9 @@ public boolean canHandle(String encoded) { @Override public Result parse(String encoded) { try { - var jwk = JWK.parse(encoded); + + var jwk = JWK.parse(encoded); // OctetKeyPairs (OKP) need special handling, as they can't be easily converted to a Java PrivateKey if (jwk instanceof OctetKeyPair okp) { return parseOctetKeyPair(okp).map(key -> key); @@ -107,6 +108,26 @@ public Result parse(String encoded) { } } + @Override + public Result parsePublic(String encoded) { + try { + var jwk = JWK.parse(encoded).toPublicJWK(); + // OctetKeyPairs (OKP) need special handling, as they can't be easily converted to a Java PrivateKey + if (jwk instanceof OctetKeyPair okp) { + return parseOctetKeyPair(okp.toPublicJWK()).map(key -> key); + } + var list = KeyConverter.toJavaKeys(List.of(jwk)); // contains an entry each for public and private key + + return list.stream() + .findFirst() + .map(Result::success) + .orElse(Result.failure(ERROR_NO_KEY)); + } catch (ParseException | NoSuchAlgorithmException | IOException | InvalidKeySpecException e) { + monitor.warning("Parser error", e); + return Result.failure("Parser error: " + e.getMessage()); + } + } + private Result parseOctetKeyPair(OctetKeyPair okp) throws NoSuchAlgorithmException, IOException, InvalidKeySpecException { var d = okp.getDecodedD(); var x = okp.getDecodedX(); diff --git a/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/PemParser.java b/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/PemParser.java index cf1d36115b3..ace181459da 100644 --- a/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/PemParser.java +++ b/core/common/lib/keys-lib/src/main/java/org/eclipse/edc/keys/keyparsers/PemParser.java @@ -62,11 +62,7 @@ public boolean canHandle(String encoded) { @Override public Result parse(String encoded) { - var matcher = PEM_FORMAT_REGEX.matcher(encoded); - if (!matcher.find()) { - return Result.failure("The given input is not valid PEM."); - } - var keypair = parseKeys(encoded); + var keypair = parsePem(encoded); if (keypair.succeeded()) { @@ -83,7 +79,38 @@ public Result parse(String encoded) { .orElseGet(() -> Result.failure("PEM-encoded structure did not contain a private key.")); } - return keypair.mapTo(); + return keypair.mapEmpty(); + } + + @Override + public Result parsePublic(String encoded) { + + var keypair = parsePem(encoded); + if (keypair.succeeded()) { + + var keyPairList = keypair.getContent(); + if (keyPairList.size() > 1) { + monitor.warning("PEM expected to contain exactly 1 key(-pair), but contained %s. Will take the first one. Please consider re-structuring your PEM document.".formatted(keyPairList.size())); + } + return keyPairList + .stream() + .filter(Objects::nonNull) // PEM strings that only contain public keys would get eliminated here + .map(keyPair -> (Key) keyPair.getPublic()) + .filter(Objects::nonNull) + .findFirst() + .map(Result::success) + .orElseGet(() -> Result.failure("PEM-encoded structure did not contain a public key.")); + } + + return keypair.mapEmpty(); + } + + private Result> parsePem(String pemEncoded) { + var matcher = PEM_FORMAT_REGEX.matcher(pemEncoded); + if (!matcher.find()) { + return Result.failure("The given input is not valid PEM."); + } + return parseKeys(pemEncoded); } /** diff --git a/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/JwkParserTest.java b/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/JwkParserTest.java index 0d9aaa21544..cb1e93cf15c 100644 --- a/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/JwkParserTest.java +++ b/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/JwkParserTest.java @@ -63,6 +63,23 @@ void parse_publicKey(JWK jwk) { assertThat(result).isSucceeded().isInstanceOf(PublicKey.class); } + + @ParameterizedTest + @ArgumentsSource(KeyProvider.class) + void parsePublic_withPublicKey(JWK jwk) { + var publickey = jwk.toPublicJWK(); + var result = parser.parsePublic(publickey.toJSONString()); + assertThat(result).isSucceeded().isInstanceOf(PublicKey.class); + } + + + @ParameterizedTest + @ArgumentsSource(KeyProvider.class) + void parsePublic_withPrivateKey(JWK jwk) { + var result = parser.parsePublic(jwk.toJSONString()); + assertThat(result).isSucceeded().isInstanceOf(PublicKey.class); + } + private static class KeyProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { diff --git a/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/PemParserTest.java b/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/PemParserTest.java index cc30002c8ed..7c7f7472219 100644 --- a/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/PemParserTest.java +++ b/core/common/lib/keys-lib/src/test/java/org/eclipse/edc/keys/keyparsers/PemParserTest.java @@ -17,6 +17,7 @@ import org.assertj.core.api.Assertions; import org.eclipse.edc.junit.testfixtures.TestUtils; import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -74,6 +75,38 @@ void parse_publicKey(String pem) { .isInstanceOf(PublicKey.class); } + + @ParameterizedTest + @ArgumentsSource(PemConvertiblePrivateKeyProvider.class) + void parsePublic_withPrivateKey(String pem) { + var result = parser.parsePublic(pem); + assertThat(result) + .isSucceeded() + .isNotNull() + .isInstanceOf(PublicKey.class); + + } + + @Test + void parsePublic_withPrivateKey_whenEd25519_shouldFail() { + var pem = TestUtils.getResourceFileContentAsString("ed25519.pem"); + var result = parser.parsePublic(pem); + assertThat(result) + .isFailed() + .detail().isEqualTo("PEM-encoded structure did not contain a public key."); + } + + + @ParameterizedTest + @ArgumentsSource(PemPublicKeyProvider.class) + void parsePublic_withPublicKey(String pem) { + + assertThat(parser.parsePublic(pem)) + .isSucceeded() + .isNotNull() + .isInstanceOf(PublicKey.class); + } + private static class PemPrivateKeyProvider implements ArgumentsProvider { @Override @@ -88,6 +121,19 @@ public Stream provideArguments(ExtensionContext context) { } } + private static class PemConvertiblePrivateKeyProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.named("RSA PrivateKey", TestUtils.getResourceFileContentAsString("rsa_2048.pem"))), + Arguments.of(Named.named("EC PrivateKey (P256)", TestUtils.getResourceFileContentAsString("ec_p256.pem"))), + Arguments.of(Named.named("EC PrivateKey (P384)", TestUtils.getResourceFileContentAsString("ec_p384.pem"))), + Arguments.of(Named.named("EC PrivateKey (P512)", TestUtils.getResourceFileContentAsString("ec_p512.pem"))) + ); + } + } + private static class PemPublicKeyProvider implements ArgumentsProvider { @Override diff --git a/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParser.java b/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParser.java index 2ed1fed38de..c0904f1b8a2 100644 --- a/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParser.java +++ b/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParser.java @@ -49,4 +49,20 @@ public interface KeyParser { * @return Either a {@link java.security.PrivateKey}, a {@link java.security.PublicKey} or a failure. */ Result parse(String encoded); + + /** + * Parses the encoded key as public key. If the encoded string is invalid, or the parser can't handle the input, + * it must return a {@link Result#failure(String)}, it must never throw an exception. + *

+ * If the given key material contains public and private key data, the parser attempts to remove the private key data, + * returning only the public part of the key as {@link java.security.PublicKey}. + * If the given key material does not contain private key data, just public key data, returns a {@link java.security.PublicKey}. In all + * other cases, a {@link Result#failure(String)} is returned, for example, when a private key cannot be converted into a public key. + * + * @param encoded serialized/encoded key material. + * @return Either a {@link java.security.PublicKey} or a failure. + */ + default Result parsePublic(String encoded) { + return parse(encoded); + } } diff --git a/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParserRegistry.java b/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParserRegistry.java index 99f4e2a2e51..a340aba24bf 100644 --- a/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParserRegistry.java +++ b/spi/common/keys-spi/src/main/java/org/eclipse/edc/keys/spi/KeyParserRegistry.java @@ -17,7 +17,6 @@ import org.eclipse.edc.spi.result.Result; import java.security.Key; -import java.security.PrivateKey; /** * Registry that holds multiple {@link KeyParser} instances that are used to deserialize a private key from their @@ -30,11 +29,21 @@ public interface KeyParserRegistry { void register(KeyParser parser); /** - * Attempts to parse the String representation of a private key into a {@link PrivateKey}. If no parser can handle + * Attempts to parse the String representation of a private key into a {@link Key}. If no parser can handle * the encoded format, or it is corrupt etc. then a failure is returned. * * @param encoded The private key in encoded format (PEM, OpenSSH, JWK, PKCS8,...) * @return a success result containing the private key, a failure if the encoded private key could not be deserialized. */ Result parse(String encoded); + + /** + * Attempts to parse the String representation of a public or private key into a {@link Key}. If no parser can handle + * the encoded format, if the encoded format contains a private key that cannot be converted to a public key, + * or if the input is corrupt etc. then a failure is returned. + * + * @param encoded The private key in encoded format (PEM, OpenSSH, JWK, PKCS8,...) + * @return a success result containing the public key, a failure if the encoded public key could not be deserialized. + */ + Result parsePublic(String encoded); }