diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionJsonRpcUnauthorizedResponse.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/ExpectLoginDisabled.java similarity index 62% rename from acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionJsonRpcUnauthorizedResponse.java rename to acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/ExpectLoginDisabled.java index 91e5ecfd630..6f68a38c105 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionJsonRpcUnauthorizedResponse.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/ExpectLoginDisabled.java @@ -12,23 +12,16 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.net; +package org.hyperledger.besu.tests.acceptance.dsl.condition.login; import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetVersionTransaction; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.login.LoginDisabledTransaction; -public class ExpectNetVersionPermissionJsonRpcUnauthorizedResponse implements Condition { - - private final NetVersionTransaction transaction; - - public ExpectNetVersionPermissionJsonRpcUnauthorizedResponse( - final NetVersionTransaction transaction) { - this.transaction = transaction; - } +public class ExpectLoginDisabled implements Condition { @Override public void verify(final Node node) { - node.execute(transaction); + node.execute(new LoginDisabledTransaction()); } } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java index 09e4e831547..234d1324b93 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/login/LoginConditions.java @@ -27,6 +27,10 @@ public Condition failure(final String username, final String password) { return new ExpectLoginUnauthorized(username, password); } + public Condition disabled() { + return new ExpectLoginDisabled(); + } + public Condition awaitResponse(final String username, final String password) { return new AwaitLoginResponse<>(new LoginTransaction(username, password)); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionException.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionException.java deleted file mode 100644 index 56bdc0fc59f..00000000000 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionPermissionException.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.tests.acceptance.dsl.condition.net; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetVersionTransaction; - -import org.web3j.protocol.exceptions.ClientConnectionException; - -public class ExpectNetVersionPermissionException implements Condition { - - private final NetVersionTransaction transaction; - private final String expectedMessage; - - public ExpectNetVersionPermissionException( - final NetVersionTransaction transaction, final String expectedMessage) { - this.transaction = transaction; - this.expectedMessage = expectedMessage; - } - - @Override - public void verify(final Node node) { - final Throwable thrown = catchThrowable(() -> node.execute(transaction)); - assertThat(thrown).isInstanceOf(RuntimeException.class); - - final Throwable cause = thrown.getCause(); - assertThat(cause).isInstanceOf(ClientConnectionException.class); - assertThat(cause.getMessage()).contains(expectedMessage); - } -} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionConnectionExceptionWithCause.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectUnauthorized.java similarity index 69% rename from acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionConnectionExceptionWithCause.java rename to acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectUnauthorized.java index 7dbc96bf390..37a182bc4af 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectNetVersionConnectionExceptionWithCause.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/ExpectUnauthorized.java @@ -19,22 +19,20 @@ import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; import org.hyperledger.besu.tests.acceptance.dsl.node.Node; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetVersionTransaction; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; -public class ExpectNetVersionConnectionExceptionWithCause implements Condition { +public class ExpectUnauthorized implements Condition { - private final NetVersionTransaction transaction; - private final Class<? extends Throwable> cause; + private static final String UNAUTHORIZED = "Unauthorized"; + private final Transaction<?> transaction; - public ExpectNetVersionConnectionExceptionWithCause( - final NetVersionTransaction transaction, final Class<? extends Throwable> cause) { + public ExpectUnauthorized(final Transaction<?> transaction) { this.transaction = transaction; - this.cause = cause; } @Override public void verify(final Node node) { final Throwable thrown = catchThrowable(() -> node.execute(transaction)); - assertThat(thrown).isInstanceOf(cause); + assertThat(thrown.getMessage()).contains(UNAUTHORIZED); } } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java index 39044410241..004b12b9162 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/net/NetConditions.java @@ -35,6 +35,10 @@ public Condition netServicesOnlyJsonRpcEnabled() { return new ExpectNetServicesReturnsOnlyJsonRpcActive(transactions.netServices()); } + public Condition netServicesUnauthorized() { + return new ExpectUnauthorized(transactions.netServices()); + } + public Condition netVersion() { return new ExpectNetVersionIsNotBlank(transactions.netVersion()); } @@ -47,16 +51,8 @@ public Condition netVersionExceptional(final String expectedMessage) { return new ExpectNetVersionConnectionException(transactions.netVersion(), expectedMessage); } - public Condition netVersionExceptional(final Class<? extends Throwable> cause) { - return new ExpectNetVersionConnectionExceptionWithCause(transactions.netVersion(), cause); - } - - public Condition netVersionUnauthorizedExceptional(final String expectedMessage) { - return new ExpectNetVersionPermissionException(transactions.netVersion(), expectedMessage); - } - - public Condition netVersionUnauthorizedResponse() { - return new ExpectNetVersionPermissionJsonRpcUnauthorizedResponse(transactions.netVersion()); + public Condition netVersionUnauthorized() { + return new ExpectUnauthorized(transactions.netVersion()); } public Condition awaitPeerCountExceptional() { diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java index 5032a8cc0b3..246766603ec 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java @@ -132,6 +132,10 @@ public void startNode(final BesuNode node) { params.add("--rpc-http-authentication-credentials-file"); params.add(node.jsonRpcConfiguration().getAuthenticationCredentialsFile()); } + if (node.jsonRpcConfiguration().getAuthenticationPublicKeyFile() != null) { + params.add("--rpc-http-authentication-jwt-public-key-file"); + params.add(node.jsonRpcConfiguration().getAuthenticationPublicKeyFile().getAbsolutePath()); + } } if (node.wsRpcEnabled()) { @@ -149,6 +153,11 @@ public void startNode(final BesuNode node) { params.add("--rpc-ws-authentication-credentials-file"); params.add(node.webSocketConfiguration().getAuthenticationCredentialsFile()); } + if (node.webSocketConfiguration().getAuthenticationPublicKeyFile() != null) { + params.add("--rpc-ws-authentication-jwt-public-key-file"); + params.add( + node.webSocketConfiguration().getAuthenticationPublicKeyFile().getAbsolutePath()); + } } if (node.isMetricsEnabled()) { diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java index b9b5f3fd947..de7d09bf3d5 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java @@ -26,6 +26,7 @@ import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationProvider; +import java.io.File; import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.ArrayList; @@ -114,6 +115,19 @@ public BesuNodeConfigurationBuilder jsonRpcAuthenticationEnabled() throws URISyn return this; } + public BesuNodeConfigurationBuilder jsonRpcAuthenticationUsingPublicKeyEnabled() + throws URISyntaxException { + final File jwtPublicKey = + Paths.get(ClassLoader.getSystemResource("authentication/jwt_public_key").toURI()) + .toAbsolutePath() + .toFile(); + + this.jsonRpcConfiguration.setAuthenticationEnabled(true); + this.jsonRpcConfiguration.setAuthenticationPublicKeyFile(jwtPublicKey); + + return this; + } + public BesuNodeConfigurationBuilder webSocketConfiguration( final WebSocketConfiguration webSocketConfiguration) { this.webSocketConfiguration = webSocketConfiguration; @@ -130,7 +144,7 @@ public BesuNodeConfigurationBuilder webSocketEnabled() { final WebSocketConfiguration config = WebSocketConfiguration.createDefault(); config.setEnabled(true); config.setPort(0); - config.setHostsWhitelist(Collections.singleton("*")); + config.setHostsWhitelist(Collections.singletonList("*")); this.webSocketConfiguration = config; return this; @@ -153,6 +167,19 @@ public BesuNodeConfigurationBuilder webSocketAuthenticationEnabled() throws URIS return this; } + public BesuNodeConfigurationBuilder webSocketAuthenticationUsingPublicKeyEnabled() + throws URISyntaxException { + final File jwtPublicKey = + Paths.get(ClassLoader.getSystemResource("authentication/jwt_public_key").toURI()) + .toAbsolutePath() + .toFile(); + + this.webSocketConfiguration.setAuthenticationEnabled(true); + this.webSocketConfiguration.setAuthenticationPublicKeyFile(jwtPublicKey); + + return this; + } + public BesuNodeConfigurationBuilder permissioningConfiguration( final PermissioningConfiguration permissioningConfiguration) { this.permissioningConfiguration = Optional.of(permissioningConfiguration); diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java index d40b1140a6b..1707e047a35 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java @@ -128,7 +128,7 @@ public BesuNode createArchiveNodeNetServicesDisabled(final String name) throws I .build()); } - public BesuNode createArchiveNodeWithAuthentication(final String name) + public BesuNode createNodeWithAuthentication(final String name) throws IOException, URISyntaxException { return create( new BesuNodeConfigurationBuilder() @@ -136,16 +136,19 @@ public BesuNode createArchiveNodeWithAuthentication(final String name) .jsonRpcEnabled() .jsonRpcAuthenticationEnabled() .webSocketEnabled() + .webSocketAuthenticationEnabled() .build()); } - public BesuNode createArchiveNodeWithAuthenticationOverWebSocket(final String name) + public BesuNode createNodeWithAuthenticationUsingJwtPublicKey(final String name) throws IOException, URISyntaxException { return create( new BesuNodeConfigurationBuilder() .name(name) + .jsonRpcEnabled() + .jsonRpcAuthenticationUsingPublicKeyEnabled() .webSocketEnabled() - .webSocketAuthenticationEnabled() + .webSocketAuthenticationUsingPublicKeyEnabled() .build()); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/login/LoginDisabledTransaction.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/login/LoginDisabledTransaction.java new file mode 100644 index 00000000000..f4a556aef7c --- /dev/null +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/login/LoginDisabledTransaction.java @@ -0,0 +1,39 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.tests.acceptance.dsl.transaction.login; + +import static org.assertj.core.api.Fail.fail; + +import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; + +import java.io.IOException; + +import org.assertj.core.api.Assertions; + +public class LoginDisabledTransaction implements Transaction<Void> { + + @Override + public Void execute(final NodeRequests node) { + try { + final String loginResponse = node.login().send("user", "password"); + Assertions.assertThat(loginResponse).isEqualTo("Authentication not enabled"); + return null; + } catch (final IOException e) { + fail("Login request failed with exception: ", e); + return null; + } + } +} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java index f4b87f45af1..2b3386a0d91 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetServicesTransaction.java @@ -37,6 +37,9 @@ public Map<String, Map<String, String>> execute(final NodeRequests requestFactor } catch (final Exception e) { throw new RuntimeException(e); } + if (netServicesResponse.hasError()) { + throw new RuntimeException(netServicesResponse.getError().getMessage()); + } return netServicesResponse.getResult(); } } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java index 6cb7950ad47..8d4db1397cb 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/net/NetVersionTransaction.java @@ -30,6 +30,9 @@ public String execute(final NodeRequests node) { try { final NetVersion result = node.net().netVersion().send(); assertThat(result).isNotNull(); + if (result.hasError()) { + throw new RuntimeException(result.getError().getMessage()); + } return result.getNetVersion(); } catch (final Exception e) { throw new RuntimeException(e); diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java index 58cd487175d..5ff7dd6d3ac 100644 --- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/HttpServiceLoginAcceptanceTest.java @@ -27,39 +27,84 @@ import org.junit.Test; public class HttpServiceLoginAcceptanceTest extends AcceptanceTestBase { - private Cluster authenticatedCluster; - private BesuNode node; + private BesuNode nodeUsingAuthFile; + private BesuNode nodeUsingJwtPublicKey; + + // token with payload{"iat": 1516239022,"exp": 4729363200,"permissions": ["net:peerCount"]} + private static final String TOKEN_ALLOWING_NET_PEER_COUNT = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWl" + + "zc2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ.Y6mNV0nvjzOdqAgMgxknFAOUTKoeRAo4aifNgNrWtuXbJJgz6-" + + "H_0GvLgjlToohPiDZbBJXJJlgb4zzLLB-sRtFnGoPaMgz_d_6z958GjFD7x_Fl0HW-WrTjRNenZNfTyD86OEAf" + + "XHy-7N3OYY2a5yeDbppTJy6nnHTq9hY-ad22-oWL1RbK3T_hnUJII_uXCZ9bJggSfu5m-NNUrm3TeqdnQzIaIz" + + "DqHlL0wNZwVPB4cFGN7zKghReBpkRJ8OFlxexQ491Q5eSpuYquhef-yGCIaMfy7GVtpDSD3Y-hjOErr7gUNCUh" + + "1wlc3Rb7ru_0qNgCWTBPJeRK32GppYotwQ"; @Before public void setUp() throws IOException, URISyntaxException { final ClusterConfiguration clusterConfiguration = new ClusterConfigurationBuilder().awaitPeerDiscovery(false).build(); - authenticatedCluster = new Cluster(clusterConfiguration, net); - node = besu.createArchiveNodeWithAuthentication("node1"); - authenticatedCluster.start(node); - node.verify(login.awaitResponse("user", "badpassword")); + + nodeUsingAuthFile = besu.createNodeWithAuthentication("node1"); + nodeUsingJwtPublicKey = besu.createNodeWithAuthenticationUsingJwtPublicKey("node2"); + authenticatedCluster.start(nodeUsingAuthFile, nodeUsingJwtPublicKey); + + nodeUsingAuthFile.verify(login.awaitResponse("user", "badpassword")); + nodeUsingJwtPublicKey.verify(login.awaitResponse("user", "badpassword")); } @Test public void shouldFailLoginWithWrongCredentials() { - node.verify(login.failure("user", "badpassword")); + nodeUsingAuthFile.verify(login.failure("user", "badpassword")); } @Test public void shouldSucceedLoginWithCorrectCredentials() { - node.verify(login.success("user", "pegasys")); + nodeUsingAuthFile.verify(login.success("user", "pegasys")); } @Test public void jsonRpcMethodShouldSucceedWithAuthenticatedUserAndPermission() { final String token = - node.execute(permissioningTransactions.createSuccessfulLogin("user", "pegasys")); - node.useAuthenticationTokenInHeaderForJsonRpc(token); + nodeUsingAuthFile.execute( + permissioningTransactions.createSuccessfulLogin("user", "pegasys")); + nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token); + nodeUsingAuthFile.verify(net.awaitPeerCount(1)); + } + + @Test + public void jsonRpcMethodShouldFailOnNonPermittedMethod() { + final String token = + nodeUsingAuthFile.execute( + permissioningTransactions.createSuccessfulLogin("user", "pegasys")); + nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token); + nodeUsingAuthFile.verify(net.netVersionUnauthorized()); + nodeUsingAuthFile.verify(net.netServicesUnauthorized()); + } - node.verify(net.awaitPeerCount(0)); - node.verify(net.netVersionUnauthorizedExceptional("Unauthorized")); + @Test + public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() { + nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT); + nodeUsingJwtPublicKey.verify(net.awaitPeerCount(1)); + } + + @Test + public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldFailOnNonPermittedMethod() { + nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT); + nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized()); + nodeUsingJwtPublicKey.verify(net.netServicesUnauthorized()); + } + + @Test + public void jsonRpcMethodShouldFailWhenThereIsNoToken() { + nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized()); + nodeUsingJwtPublicKey.verify(net.netServicesUnauthorized()); + } + + @Test + public void loginShouldBeDisabledWhenUsingExternalJwtPublicKey() { + nodeUsingJwtPublicKey.verify(login.disabled()); } @Override diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java index 96e7872138b..e81cdad8647 100644 --- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java @@ -16,6 +16,9 @@ import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; +import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.Cluster; +import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfiguration; +import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfigurationBuilder; import java.io.IOException; import java.net.URISyntaxException; @@ -24,31 +27,91 @@ import org.junit.Test; public class WebsocketServiceLoginAcceptanceTest extends AcceptanceTestBase { - private BesuNode node; + private BesuNode nodeUsingAuthFile; + private BesuNode nodeUsingJwtPublicKey; + private Cluster authenticatedCluster; + + // token with payload{"iat": 1516239022,"exp": 4729363200,"permissions": ["net:peerCount"]} + private static final String TOKEN_ALLOWING_NET_PEER_COUNT = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWl" + + "zc2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ.Y6mNV0nvjzOdqAgMgxknFAOUTKoeRAo4aifNgNrWtuXbJJgz6-" + + "H_0GvLgjlToohPiDZbBJXJJlgb4zzLLB-sRtFnGoPaMgz_d_6z958GjFD7x_Fl0HW-WrTjRNenZNfTyD86OEAf" + + "XHy-7N3OYY2a5yeDbppTJy6nnHTq9hY-ad22-oWL1RbK3T_hnUJII_uXCZ9bJggSfu5m-NNUrm3TeqdnQzIaIz" + + "DqHlL0wNZwVPB4cFGN7zKghReBpkRJ8OFlxexQ491Q5eSpuYquhef-yGCIaMfy7GVtpDSD3Y-hjOErr7gUNCUh" + + "1wlc3Rb7ru_0qNgCWTBPJeRK32GppYotwQ"; @Before public void setUp() throws IOException, URISyntaxException { - node = besu.createArchiveNodeWithAuthenticationOverWebSocket("node1"); - cluster.start(node); - node.useWebSocketsForJsonRpc(); + final ClusterConfiguration clusterConfiguration = + new ClusterConfigurationBuilder().awaitPeerDiscovery(false).build(); + authenticatedCluster = new Cluster(clusterConfiguration, net); + + nodeUsingAuthFile = besu.createNodeWithAuthentication("node1"); + nodeUsingJwtPublicKey = besu.createNodeWithAuthenticationUsingJwtPublicKey("node2"); + authenticatedCluster.start(nodeUsingAuthFile, nodeUsingJwtPublicKey); + + nodeUsingAuthFile.useWebSocketsForJsonRpc(); + nodeUsingJwtPublicKey.useWebSocketsForJsonRpc(); + nodeUsingAuthFile.verify(login.awaitResponse("user", "badpassword")); + nodeUsingJwtPublicKey.verify(login.awaitResponse("user", "badpassword")); } @Test public void shouldFailLoginWithWrongCredentials() { - node.verify(login.failure("user", "badpassword")); + nodeUsingAuthFile.verify(login.failure("user", "badpassword")); } @Test public void shouldSucceedLoginWithCorrectCredentials() { - node.verify(login.success("user", "pegasys")); + nodeUsingAuthFile.verify(login.success("user", "pegasys")); } @Test public void jsonRpcMethodShouldSucceedWithAuthenticatedUserAndPermission() { final String token = - node.execute(permissioningTransactions.createSuccessfulLogin("user", "pegasys")); - node.useAuthenticationTokenInHeaderForJsonRpc(token); - node.verify(net.awaitPeerCount(0)); - node.verify(net.netVersionUnauthorizedResponse()); + nodeUsingAuthFile.execute( + permissioningTransactions.createSuccessfulLogin("user", "pegasys")); + nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token); + nodeUsingAuthFile.verify(net.awaitPeerCount(1)); + } + + @Test + public void jsonRpcMethodShouldFailOnNonPermittedMethod() { + final String token = + nodeUsingAuthFile.execute( + permissioningTransactions.createSuccessfulLogin("user", "pegasys")); + nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token); + nodeUsingAuthFile.verify(net.netVersionUnauthorized()); + nodeUsingAuthFile.verify(net.netServicesUnauthorized()); + } + + @Test + public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() { + nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT); + nodeUsingJwtPublicKey.verify(net.awaitPeerCount(1)); + } + + @Test + public void externalJwtPublicKeyUsedOnJsonRpcMethodShouldFailOnNonPermittedMethod() { + nodeUsingJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(TOKEN_ALLOWING_NET_PEER_COUNT); + nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized()); + nodeUsingAuthFile.verify(net.netServicesUnauthorized()); + } + + @Test + public void jsonRpcMethodShouldFailWhenThereIsNoToken() { + nodeUsingJwtPublicKey.verify(net.netVersionUnauthorized()); + nodeUsingJwtPublicKey.verify(net.netServicesUnauthorized()); + } + + @Test + public void loginShouldBeDisabledWhenUsingExternalJwtPublicKey() { + nodeUsingJwtPublicKey.verify(login.disabled()); + } + + @Override + public void tearDownAcceptanceTestBase() { + authenticatedCluster.stop(); + super.tearDownAcceptanceTestBase(); } } diff --git a/acceptance-tests/tests/src/test/resources/authentication/jwt_public_key b/acceptance-tests/tests/src/test/resources/authentication/jwt_public_key new file mode 100644 index 00000000000..9107cf29702 --- /dev/null +++ b/acceptance-tests/tests/src/test/resources/authentication/jwt_public_key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6tMhjogMulRbYby7bCL +rFhukDnxvm4XR3KSXLKdLLQHHyouMOQaLac9M+/Z1KkIpqfZPjLfW2/yUg2IKx4T +dvFVzbVq17X6dq49ZS8jJtb8l2+Vius4d3LnpvxCOematRG9Acn+2qLwC+sK7RPY +OxEqKPU5LNBH1C0FfviazY5jkixBFICzIq/SyyRnGX+iIONnNsu0TlhWVLSlZbg5 +NYf4cAzu/1d5MgspyZwnRo468gqaak3wQzkmk69Z25L1N7TXZvk2b7rT7/ssFnt+ +//fKVpD6qkQ3OopD+7gOziAYUxChw6RUWekV+uRgNADQhaqV6wDdogBz77wTJedV +YwIDAQAB +-----END PUBLIC KEY----- diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 7c269ea5b7b..1da7a15dbea 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -1151,12 +1151,15 @@ private JsonRpcConfiguration jsonRpcConfiguration() { "--rpc-http-host", "--rpc-http-port", "--rpc-http-authentication-enabled", - "--rpc-http-authentication-credentials-file")); + "--rpc-http-authentication-credentials-file", + "--rpc-http-authentication-public-key-file")); - if (isRpcHttpAuthenticationEnabled && rpcHttpAuthenticationCredentialsFile() == null) { + if (isRpcHttpAuthenticationEnabled + && rpcHttpAuthenticationCredentialsFile() == null + && rpcHttpAuthenticationPublicKeyFile() == null) { throw new ParameterException( commandLine, - "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file"); + "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file"); } final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); @@ -1168,6 +1171,7 @@ private JsonRpcConfiguration jsonRpcConfiguration() { jsonRpcConfiguration.setHostsWhitelist(hostsWhitelist); jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled); jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile()); + jsonRpcConfiguration.setAuthenticationPublicKeyFile(rpcHttpAuthenticationPublicKeyFile()); return jsonRpcConfiguration; } @@ -1184,12 +1188,15 @@ private WebSocketConfiguration webSocketConfiguration() { "--rpc-ws-host", "--rpc-ws-port", "--rpc-ws-authentication-enabled", - "--rpc-ws-authentication-credentials-file")); + "--rpc-ws-authentication-credentials-file", + "--rpc-ws-authentication-public-key-file")); - if (isRpcWsAuthenticationEnabled && rpcWsAuthenticationCredentialsFile() == null) { + if (isRpcWsAuthenticationEnabled + && rpcWsAuthenticationCredentialsFile() == null + && rpcWsAuthenticationPublicKeyFile() == null) { throw new ParameterException( commandLine, - "Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file"); + "Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file"); } final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); @@ -1200,6 +1207,7 @@ private WebSocketConfiguration webSocketConfiguration() { webSocketConfiguration.setAuthenticationEnabled(isRpcWsAuthenticationEnabled); webSocketConfiguration.setAuthenticationCredentialsFile(rpcWsAuthenticationCredentialsFile()); webSocketConfiguration.setHostsWhitelist(hostsWhitelist); + webSocketConfiguration.setAuthenticationPublicKeyFile(rpcWsAuthenticationPublicKeyFile()); return webSocketConfiguration; } @@ -1680,6 +1688,24 @@ private String rpcWsAuthenticationCredentialsFile() { return filename; } + private File rpcHttpAuthenticationPublicKeyFile() { + if (isDocker) { + final File keyFile = new File(DOCKER_RPC_HTTP_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION); + return keyFile.exists() ? keyFile : null; + } else { + return standaloneCommands.rpcHttpAuthenticationPublicKeyFile; + } + } + + private File rpcWsAuthenticationPublicKeyFile() { + if (isDocker) { + final File keyFile = new File(DOCKER_RPC_WS_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION); + return keyFile.exists() ? keyFile : null; + } else { + return standaloneCommands.rpcWsAuthenticationPublicKeyFile; + } + } + private String nodePermissionsConfigFile() { return permissionsConfigFile(standaloneCommands.nodePermissionsConfigFile); } diff --git a/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java b/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java index 3ae903cd490..3e3cd4c21bc 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java @@ -55,6 +55,9 @@ public interface DefaultCommandValues { "/etc/besu/rpc_http_auth_config.toml"; String DOCKER_RPC_WS_AUTHENTICATION_CREDENTIALS_FILE_LOCATION = "/etc/besu/rpc_ws_auth_config.toml"; + String DOCKER_RPC_HTTP_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION = + "/etc/besu/rpc_http_auth_public_key"; + String DOCKER_RPC_WS_AUTHENTICATION_PUBLIC_KEY_FILE_LOCATION = "/etc/besu/rpc_ws_auth_public_key"; String DOCKER_PRIVACY_PUBLIC_KEY_FILE = "/etc/besu/privacy_public_key"; String DOCKER_PERMISSIONS_CONFIG_FILE_LOCATION = "/etc/besu/permissions_config.toml"; String PERMISSIONING_CONFIG_LOCATION = "permissions_config.toml"; diff --git a/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java index 18ade6d464d..7990a8f79d1 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/StandaloneCommand.java @@ -65,6 +65,13 @@ class StandaloneCommand implements DefaultCommandValues { arity = "1") String rpcHttpAuthenticationCredentialsFile = null; + @CommandLine.Option( + names = {"--rpc-http-authentication-jwt-public-key-file"}, + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "JWT public key file for JSON-RPC HTTP authentication", + arity = "1") + final File rpcHttpAuthenticationPublicKeyFile = null; + @CommandLine.Option( names = {"--rpc-ws-authentication-credentials-file"}, paramLabel = MANDATORY_FILE_FORMAT_HELP, @@ -73,6 +80,13 @@ class StandaloneCommand implements DefaultCommandValues { arity = "1") String rpcWsAuthenticationCredentialsFile = null; + @CommandLine.Option( + names = {"--rpc-ws-authentication-jwt-public-key-file"}, + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "JWT public key file for JSON-RPC WebSocket authentication", + arity = "1") + final File rpcWsAuthenticationPublicKeyFile = null; + @CommandLine.Option( names = {"--privacy-public-key-file"}, description = "The enclave's public key file") diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index bd627dd82ad..06fb6c0d9b5 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -36,6 +36,7 @@ import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import org.hyperledger.besu.BesuInfo; @@ -2771,6 +2772,32 @@ public void rpcWsAuthCredentialsFileOptionDisabledUnderDocker() { assertThat(commandOutput.toString()).isEmpty(); } + @Test + public void rpcHttpAuthPublicKeyFileOptionDisabledUnderDocker() { + System.setProperty("besu.docker", "true"); + + assumeFalse(isFullInstantiation()); + + final Path path = Paths.get("."); + parseCommand("--rpc-http-authentication-public-key-file", path.toString()); + assertThat(commandErrorOutput.toString()) + .startsWith("Unknown options: --rpc-http-authentication-public-key-file, ."); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void rpcWsAuthPublicKeyFileOptionDisabledUnderDocker() { + System.setProperty("besu.docker", "true"); + + assumeFalse(isFullInstantiation()); + + final Path path = Paths.get("."); + parseCommand("--rpc-ws-authentication-public-key-file", path.toString()); + assertThat(commandErrorOutput.toString()) + .startsWith("Unknown options: --rpc-ws-authentication-public-key-file, ."); + assertThat(commandOutput.toString()).isEmpty(); + } + @Test public void permissionsConfigFileOptionDisabledUnderDocker() { System.setProperty("besu.docker", "true"); @@ -2995,4 +3022,50 @@ public void requiredBlocksMulpleBlocksTwoArgs() { assertThat(requiredBlocksArg.getValue()) .containsEntry(block2, Hash.fromHexStringLenient(hash2)); } + + @Test + public void httpAuthenticationPublicKeyIsConfigured() throws IOException { + final Path publicKey = Files.createTempFile("public_key", ""); + parseCommand("--rpc-http-authentication-jwt-public-key-file", publicKey.toString()); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath()) + .isEqualTo(publicKey.toString()); + } + + @Test + public void httpAuthenticationWithoutRequiredConfiguredOptionsMustFail() { + parseCommand("--rpc-http-enabled", "--rpc-http-authentication-enabled"); + + verifyNoInteractions(mockRunnerBuilder); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .contains( + "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file"); + } + + @Test + public void wsAuthenticationPublicKeyIsConfigured() throws IOException { + final Path publicKey = Files.createTempFile("public_key", ""); + parseCommand("--rpc-ws-authentication-jwt-public-key-file", publicKey.toString()); + + verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(wsRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath()) + .isEqualTo(publicKey.toString()); + } + + @Test + public void wsAuthenticationWithoutRequiredConfiguredOptionsMustFail() { + parseCommand("--rpc-ws-enabled", "--rpc-ws-authentication-enabled"); + + verifyNoInteractions(mockRunnerBuilder); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .contains( + "Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file"); + } } diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index 602b286651d..1f26adb0022 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -50,6 +50,7 @@ rpc-http-apis=["DEBUG","ETH"] rpc-http-cors-origins=["none"] rpc-http-authentication-enabled=false rpc-http-authentication-credentials-file="none" +rpc-http-authentication-jwt-public-key-file="none" # GRAPHQL HTTP graphql-http-enabled=false @@ -65,6 +66,7 @@ rpc-ws-host="9.10.11.12" rpc-ws-port=9101 rpc-ws-authentication-enabled=false rpc-ws-authentication-credentials-file="none" +rpc-ws-authentication-jwt-public-key-file="none" # Prometheus Metrics Endpoint metrics-enabled=false diff --git a/ethereum/api/build.gradle b/ethereum/api/build.gradle index a36486030ee..bd4f690f770 100644 --- a/ethereum/api/build.gradle +++ b/ethereum/api/build.gradle @@ -50,6 +50,7 @@ dependencies { implementation 'io.vertx:vertx-web' implementation 'io.vertx:vertx-auth-jwt' implementation 'io.vertx:vertx-unit' + implementation 'org.bouncycastle:bcprov-jdk15on' testImplementation 'com.squareup.okhttp3:okhttp' testImplementation 'junit:junit' diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java index 7f15c6b8037..4b1fd0c1fdb 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -35,6 +36,7 @@ public class JsonRpcConfiguration { private List<String> hostsWhitelist = Arrays.asList("localhost", "127.0.0.1"); private boolean authenticationEnabled = false; private String authenticationCredentialsFile; + private File authenticationPublicKeyFile; public static JsonRpcConfiguration createDefault() { final JsonRpcConfiguration config = new JsonRpcConfiguration(); @@ -102,6 +104,30 @@ public void setHostsWhitelist(final List<String> hostsWhitelist) { this.hostsWhitelist = hostsWhitelist; } + public boolean isAuthenticationEnabled() { + return authenticationEnabled; + } + + public void setAuthenticationEnabled(final boolean authenticationEnabled) { + this.authenticationEnabled = authenticationEnabled; + } + + public void setAuthenticationCredentialsFile(final String authenticationCredentialsFile) { + this.authenticationCredentialsFile = authenticationCredentialsFile; + } + + public String getAuthenticationCredentialsFile() { + return authenticationCredentialsFile; + } + + public File getAuthenticationPublicKeyFile() { + return authenticationPublicKeyFile; + } + + public void setAuthenticationPublicKeyFile(final File authenticationPublicKeyFile) { + this.authenticationPublicKeyFile = authenticationPublicKeyFile; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -113,6 +139,7 @@ public String toString() { .add("rpcApis", rpcApis) .add("authenticationEnabled", authenticationEnabled) .add("authenticationCredentialsFile", authenticationCredentialsFile) + .add("authenticationPublicKeyFile", authenticationPublicKeyFile) .toString(); } @@ -127,30 +154,26 @@ public boolean equals(final Object o) { final JsonRpcConfiguration that = (JsonRpcConfiguration) o; return enabled == that.enabled && port == that.port + && authenticationEnabled == that.authenticationEnabled && Objects.equals(host, that.host) && Objects.equals(corsAllowedDomains, that.corsAllowedDomains) + && Objects.equals(rpcApis, that.rpcApis) && Objects.equals(hostsWhitelist, that.hostsWhitelist) - && Objects.equals(rpcApis, that.rpcApis); + && Objects.equals(authenticationCredentialsFile, that.authenticationCredentialsFile) + && Objects.equals(authenticationPublicKeyFile, that.authenticationPublicKeyFile); } @Override public int hashCode() { - return Objects.hash(enabled, port, host, corsAllowedDomains, hostsWhitelist, rpcApis); - } - - public boolean isAuthenticationEnabled() { - return authenticationEnabled; - } - - public void setAuthenticationEnabled(final boolean authenticationEnabled) { - this.authenticationEnabled = authenticationEnabled; - } - - public void setAuthenticationCredentialsFile(final String authenticationCredentialsFile) { - this.authenticationCredentialsFile = authenticationCredentialsFile; - } - - public String getAuthenticationCredentialsFile() { - return authenticationCredentialsFile; + return Objects.hash( + enabled, + port, + host, + corsAllowedDomains, + rpcApis, + hostsWhitelist, + authenticationEnabled, + authenticationCredentialsFile, + authenticationPublicKeyFile); } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java index 913f3f7a7a6..7e132b8e58e 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationService.java @@ -17,10 +17,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; +import java.io.File; import java.util.Optional; import javax.annotation.Nullable; @@ -30,7 +27,6 @@ import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.AuthProvider; -import io.vertx.ext.auth.PubSecKeyOptions; import io.vertx.ext.auth.User; import io.vertx.ext.auth.jwt.JWTAuth; import io.vertx.ext.auth.jwt.JWTAuthOptions; @@ -42,12 +38,13 @@ public class AuthenticationService { private final JWTAuth jwtAuthProvider; @VisibleForTesting public final JWTAuthOptions jwtAuthOptions; - private final AuthProvider credentialAuthProvider; + private final Optional<AuthProvider> credentialAuthProvider; + private static final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory(); private AuthenticationService( final JWTAuth jwtAuthProvider, final JWTAuthOptions jwtAuthOptions, - final AuthProvider credentialAuthProvider) { + final Optional<AuthProvider> credentialAuthProvider) { this.jwtAuthProvider = jwtAuthProvider; this.jwtAuthOptions = jwtAuthOptions; this.credentialAuthProvider = credentialAuthProvider; @@ -65,25 +62,11 @@ private AuthenticationService( */ public static Optional<AuthenticationService> create( final Vertx vertx, final JsonRpcConfiguration config) { - final Optional<JWTAuthOptions> jwtAuthOptions = - makeJwtAuthOptions( - config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); - if (!jwtAuthOptions.isPresent()) { - return Optional.empty(); - } - - final Optional<AuthProvider> credentialAuthProvider = - makeCredentialAuthProvider( - vertx, config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); - if (!credentialAuthProvider.isPresent()) { - return Optional.empty(); - } - - return Optional.of( - new AuthenticationService( - jwtAuthOptions.map(o -> JWTAuth.create(vertx, o)).get(), - jwtAuthOptions.get(), - credentialAuthProvider.get())); + return create( + vertx, + config.isAuthenticationEnabled(), + config.getAuthenticationCredentialsFile(), + config.getAuthenticationPublicKeyFile()); } /** @@ -98,55 +81,35 @@ public static Optional<AuthenticationService> create( */ public static Optional<AuthenticationService> create( final Vertx vertx, final WebSocketConfiguration config) { - final Optional<JWTAuthOptions> jwtAuthOptions = - makeJwtAuthOptions( - config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); - if (!jwtAuthOptions.isPresent()) { + return create( + vertx, + config.isAuthenticationEnabled(), + config.getAuthenticationCredentialsFile(), + config.getAuthenticationPublicKeyFile()); + } + + private static Optional<AuthenticationService> create( + final Vertx vertx, + final boolean authenticationEnabled, + final String authenticationCredentialsFile, + final File authenticationPublicKeyFile) { + if (!authenticationEnabled + && authenticationCredentialsFile == null + && authenticationPublicKeyFile == null) { return Optional.empty(); } + final JWTAuthOptions jwtAuthOptions = + authenticationPublicKeyFile == null + ? jwtAuthOptionsFactory.createWithGeneratedKeyPair() + : jwtAuthOptionsFactory.createForExternalPublicKey(authenticationPublicKeyFile); + final Optional<AuthProvider> credentialAuthProvider = - makeCredentialAuthProvider( - vertx, config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); - if (!credentialAuthProvider.isPresent()) { - return Optional.empty(); - } + makeCredentialAuthProvider(vertx, authenticationEnabled, authenticationCredentialsFile); return Optional.of( new AuthenticationService( - jwtAuthOptions.map(o -> JWTAuth.create(vertx, o)).get(), - jwtAuthOptions.get(), - credentialAuthProvider.get())); - } - - private static Optional<JWTAuthOptions> makeJwtAuthOptions( - final boolean authenticationEnabled, @Nullable final String authenticationCredentialsFile) { - if (authenticationEnabled && authenticationCredentialsFile != null) { - final KeyPairGenerator keyGenerator; - try { - keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); - } catch (final NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - - final KeyPair keypair = keyGenerator.generateKeyPair(); - - final JWTAuthOptions jwtAuthOptions = - new JWTAuthOptions() - .setPermissionsClaimKey("permissions") - .addPubSecKey( - new PubSecKeyOptions() - .setAlgorithm("RS256") - .setPublicKey( - Base64.getEncoder().encodeToString(keypair.getPublic().getEncoded())) - .setSecretKey( - Base64.getEncoder().encodeToString(keypair.getPrivate().getEncoded()))); - - return Optional.of(jwtAuthOptions); - } else { - return Optional.empty(); - } + JWTAuth.create(vertx, jwtAuthOptions), jwtAuthOptions, credentialAuthProvider)); } private static Optional<AuthProvider> makeCredentialAuthProvider( @@ -181,6 +144,15 @@ public static void handleDisabledLogin(final RoutingContext routingContext) { * @param routingContext Routing context associated with this request */ public void handleLogin(final RoutingContext routingContext) { + if (credentialAuthProvider.isPresent()) { + login(routingContext, credentialAuthProvider.get()); + } else { + handleDisabledLogin(routingContext); + } + } + + private void login( + final RoutingContext routingContext, final AuthProvider credentialAuthProvider) { final JsonObject requestBody = routingContext.getBodyAsJson(); if (requestBody == null) { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java index 16a486c6a70..0238b69a3f9 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java @@ -71,7 +71,7 @@ public static void getUser( final String token, final Handler<Optional<User>> handler) { try { - if (!authenticationService.isPresent()) { + if (authenticationService.isEmpty()) { handler.handle(Optional.empty()); } else { authenticationService @@ -80,8 +80,14 @@ public static void getUser( .authenticate( new JsonObject().put("jwt", token), (r) -> { - final User user = r.result(); - handler.handle(Optional.of(user)); + if (r.succeeded()) { + final Optional<User> user = Optional.ofNullable(r.result()); + validateExpExists(user); + handler.handle(user); + } else { + LOG.debug("Invalid JWT token", r.cause()); + handler.handle(Optional.empty()); + } }); } } catch (Exception e) { @@ -89,6 +95,12 @@ public static void getUser( } } + private static void validateExpExists(final Optional<User> user) { + if (!user.map(User::principal).map(p -> p.containsKey("exp")).orElse(false)) { + throw new IllegalStateException("Invalid JWT doesn't have expiry"); + } + } + public static String getJwtTokenFromAuthorizationHeaderValue(final String value) { if (value != null) { final String bearerSchemaName = "Bearer "; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactory.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactory.java new file mode 100644 index 00000000000..2be5695dc88 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.authentication; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import io.vertx.ext.auth.PubSecKeyOptions; +import io.vertx.ext.auth.jwt.JWTAuthOptions; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; + +public class JWTAuthOptionsFactory { + + private static final String ALGORITHM = "RS256"; + private static final String PERMISSIONS = "permissions"; + + public JWTAuthOptions createForExternalPublicKey(final File externalPublicKeyFile) { + final byte[] externalJwtPublicKey = readPublicKey(externalPublicKeyFile); + final String base64EncodedPublicKey = Base64.getEncoder().encodeToString(externalJwtPublicKey); + return new JWTAuthOptions() + .setPermissionsClaimKey(PERMISSIONS) + .addPubSecKey( + new PubSecKeyOptions().setAlgorithm(ALGORITHM).setPublicKey(base64EncodedPublicKey)); + } + + public JWTAuthOptions createWithGeneratedKeyPair() { + final KeyPair keypair = generateJwtKeyPair(); + return new JWTAuthOptions() + .setPermissionsClaimKey(PERMISSIONS) + .addPubSecKey( + new PubSecKeyOptions() + .setAlgorithm(ALGORITHM) + .setPublicKey(Base64.getEncoder().encodeToString(keypair.getPublic().getEncoded())) + .setSecretKey( + Base64.getEncoder().encodeToString(keypair.getPrivate().getEncoded()))); + } + + private byte[] readPublicKey(final File publicKeyFile) { + try (final BufferedReader reader = Files.newBufferedReader(publicKeyFile.toPath(), UTF_8); + final PemReader pemReader = new PemReader(reader)) { + final PemObject pemObject = pemReader.readPemObject(); + if (pemObject == null) { + throw new IllegalStateException("Authentication RPC public key file format is invalid"); + } + return pemObject.getContent(); + } catch (IOException e) { + throw new IllegalStateException("Authentication RPC public key could not be read", e); + } + } + + private KeyPair generateJwtKeyPair() { + final KeyPairGenerator keyGenerator; + try { + keyGenerator = KeyPairGenerator.getInstance("RSA"); + keyGenerator.initialize(2048); + } catch (final NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + return keyGenerator.generateKeyPair(); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java index 3ff98c99b09..6bfdd4b3da0 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java @@ -17,6 +17,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; +import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -37,7 +38,8 @@ public class WebSocketConfiguration { private List<RpcApi> rpcApis; private boolean authenticationEnabled = false; private String authenticationCredentialsFile; - private Collection<String> hostsWhitelist = Collections.singletonList("localhost"); + private List<String> hostsWhitelist = Arrays.asList("localhost", "127.0.0.1"); + private File authenticationPublicKeyFile; public static WebSocketConfiguration createDefault() { final WebSocketConfiguration config = new WebSocketConfiguration(); @@ -82,38 +84,6 @@ public void setRpcApis(final List<RpcApi> rpcApis) { this.rpcApis = rpcApis; } - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("enabled", enabled) - .add("port", port) - .add("host", host) - .add("rpcApis", rpcApis) - .add("authenticationEnabled", authenticationEnabled) - .add("authenticationCredentialsFile", authenticationCredentialsFile) - .toString(); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final WebSocketConfiguration that = (WebSocketConfiguration) o; - return enabled == that.enabled - && port == that.port - && Objects.equals(host, that.host) - && Objects.equals(rpcApis, that.rpcApis); - } - - @Override - public int hashCode() { - return Objects.hash(enabled, port, host, rpcApis); - } - public boolean isAuthenticationEnabled() { return authenticationEnabled; } @@ -130,11 +100,65 @@ public String getAuthenticationCredentialsFile() { return authenticationCredentialsFile; } - public void setHostsWhitelist(final Collection<String> hostsWhitelist) { + public void setHostsWhitelist(final List<String> hostsWhitelist) { this.hostsWhitelist = hostsWhitelist; } public Collection<String> getHostsWhitelist() { return Collections.unmodifiableCollection(this.hostsWhitelist); } + + public File getAuthenticationPublicKeyFile() { + return authenticationPublicKeyFile; + } + + public void setAuthenticationPublicKeyFile(final File authenticationPublicKeyFile) { + this.authenticationPublicKeyFile = authenticationPublicKeyFile; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final WebSocketConfiguration that = (WebSocketConfiguration) o; + return enabled == that.enabled + && port == that.port + && authenticationEnabled == that.authenticationEnabled + && Objects.equals(host, that.host) + && Objects.equals(rpcApis, that.rpcApis) + && Objects.equals(authenticationCredentialsFile, that.authenticationCredentialsFile) + && Objects.equals(hostsWhitelist, that.hostsWhitelist) + && Objects.equals(authenticationPublicKeyFile, that.authenticationPublicKeyFile); + } + + @Override + public int hashCode() { + return Objects.hash( + enabled, + port, + host, + rpcApis, + authenticationEnabled, + authenticationCredentialsFile, + hostsWhitelist, + authenticationPublicKeyFile); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("enabled", enabled) + .add("port", port) + .add("host", host) + .add("rpcApis", rpcApis) + .add("authenticationEnabled", authenticationEnabled) + .add("authenticationCredentialsFile", authenticationCredentialsFile) + .add("hostsWhitelist", hostsWhitelist) + .add("authenticationPublicKeyFile", authenticationPublicKeyFile) + .toString(); + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java index 74b387f8782..b33c2d7cdde 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtilsTest.java @@ -15,11 +15,30 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.authentication; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Optional; + +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.jwt.JWTAuth; +import io.vertx.ext.auth.jwt.JWTAuthOptions; +import io.vertx.ext.auth.jwt.impl.JWTAuthProviderImpl; import org.junit.Test; public class AuthenticationUtilsTest { + private static String INVALID_TOKEN_WITHOUT_EXP = + "ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn" + + "0.eyJpYXQiOjE1MTYyMzkwMjIsInBlcm1pc3Npb25zIjpbIm5ldDpwZWVyQ291bnQiXX0"; + private static String VALID_TOKEN = + "ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0.eyJpYXQiOjE1" + + "MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWlzc2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ"; + private static String VALID_TOKEN_DECODED_PAYLOAD = + "{\"iat\": 1516239022,\"exp\": 4729363200," + "\"permissions\": [\"net:peerCount\"]}"; + @Test public void getJwtTokenFromNullStringShouldReturnNull() { final String headerValue = null; @@ -55,4 +74,43 @@ public void getJwtTokenFromValidAuthorizationHeaderValueShouldReturnToken() { assertThat(token).isEqualTo("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9"); } + + @Test + public void getUserFailsIfTokenDoesNotHaveExpiryClaim() { + final AuthenticationService authenticationService = mock(AuthenticationService.class); + final JWTAuth jwtAuth = new JWTAuthProviderImpl(null, new JWTAuthOptions()); + final StubUserHandler handler = new StubUserHandler(); + when(authenticationService.getJwtAuthProvider()).thenReturn(jwtAuth); + + AuthenticationUtils.getUser( + Optional.of(authenticationService), INVALID_TOKEN_WITHOUT_EXP, handler); + + assertThat(handler.getEvent()).isEmpty(); + } + + @Test + public void getUserSucceedsWithValidToken() { + final AuthenticationService authenticationService = mock(AuthenticationService.class); + final JWTAuth jwtAuth = new JWTAuthProviderImpl(null, new JWTAuthOptions()); + final StubUserHandler handler = new StubUserHandler(); + when(authenticationService.getJwtAuthProvider()).thenReturn(jwtAuth); + + AuthenticationUtils.getUser(Optional.of(authenticationService), VALID_TOKEN, handler); + + assertThat(handler.getEvent().get().principal()) + .isEqualTo(new JsonObject(VALID_TOKEN_DECODED_PAYLOAD)); + } + + private static class StubUserHandler implements Handler<Optional<User>> { + private Optional<User> event; + + @Override + public void handle(final Optional<User> event) { + this.event = event; + } + + public Optional<User> getEvent() { + return event; + } + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactoryTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactoryTest.java new file mode 100644 index 00000000000..77d9406938f --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/JWTAuthOptionsFactoryTest.java @@ -0,0 +1,103 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.vertx.ext.auth.PubSecKeyOptions; +import io.vertx.ext.auth.jwt.JWTAuthOptions; +import org.junit.Test; + +public class JWTAuthOptionsFactoryTest { + + private static final String JWT_PUBLIC_KEY = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6tMhjogMulRbYby7bCL" + + "rFhukDnxvm4XR3KSXLKdLLQHHyouMOQaLac9M+/Z1KkIpqfZPjLfW2/yUg2IKx4T" + + "dvFVzbVq17X6dq49ZS8jJtb8l2+Vius4d3LnpvxCOematRG9Acn+2qLwC+sK7RPY" + + "OxEqKPU5LNBH1C0FfviazY5jkixBFICzIq/SyyRnGX+iIONnNsu0TlhWVLSlZbg5" + + "NYf4cAzu/1d5MgspyZwnRo468gqaak3wQzkmk69Z25L1N7TXZvk2b7rT7/ssFnt+" + + "//fKVpD6qkQ3OopD+7gOziAYUxChw6RUWekV+uRgNADQhaqV6wDdogBz77wTJedV" + + "YwIDAQAB"; + + @Test + public void createsOptionsWithGeneratedKeyPair() { + final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory(); + final JWTAuthOptions jwtAuthOptions = jwtAuthOptionsFactory.createWithGeneratedKeyPair(); + + assertThat(jwtAuthOptions.getPubSecKeys()).isNotNull(); + assertThat(jwtAuthOptions.getPubSecKeys()).hasSize(1); + assertThat(jwtAuthOptions.getPubSecKeys().get(0).getAlgorithm()).isEqualTo("RS256"); + assertThat(jwtAuthOptions.getPubSecKeys().get(0).getPublicKey()).isNotEmpty(); + assertThat(jwtAuthOptions.getPubSecKeys().get(0).getSecretKey()).isNotEmpty(); + } + + @Test + public void createsOptionsWithGeneratedKeyPairThatIsDifferentEachTime() { + final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory(); + final JWTAuthOptions jwtAuthOptions1 = jwtAuthOptionsFactory.createWithGeneratedKeyPair(); + final JWTAuthOptions jwtAuthOptions2 = jwtAuthOptionsFactory.createWithGeneratedKeyPair(); + + final PubSecKeyOptions pubSecKeyOptions1 = jwtAuthOptions1.getPubSecKeys().get(0); + final PubSecKeyOptions pubSecKeyOptions2 = jwtAuthOptions2.getPubSecKeys().get(0); + assertThat(pubSecKeyOptions1.getPublicKey()).isNotEqualTo(pubSecKeyOptions2.getPublicKey()); + assertThat(pubSecKeyOptions1.getSecretKey()).isNotEqualTo(pubSecKeyOptions2.getSecretKey()); + } + + @Test + public void createsOptionsUsingPublicKeyFile() throws URISyntaxException { + final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory(); + final File enclavePublicKeyFile = + Paths.get(ClassLoader.getSystemResource("authentication/jwt_public_key").toURI()) + .toAbsolutePath() + .toFile(); + + final JWTAuthOptions jwtAuthOptions = + jwtAuthOptionsFactory.createForExternalPublicKey(enclavePublicKeyFile); + assertThat(jwtAuthOptions.getPubSecKeys()).hasSize(1); + assertThat(jwtAuthOptions.getPubSecKeys().get(0).getAlgorithm()).isEqualTo("RS256"); + assertThat(jwtAuthOptions.getPubSecKeys().get(0).getSecretKey()).isNull(); + assertThat(jwtAuthOptions.getPubSecKeys().get(0).getPublicKey()).isEqualTo(JWT_PUBLIC_KEY); + } + + @Test + public void failsToCreateOptionsWhenPublicKeyFileDoesNotExist() { + final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory(); + final File enclavePublicKeyFile = new File("doesNotExist"); + + assertThatThrownBy(() -> jwtAuthOptionsFactory.createForExternalPublicKey(enclavePublicKeyFile)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Authentication RPC public key could not be read"); + } + + @Test + public void failsToCreateOptionsWhenPublicKeyFileIsInvalid() throws IOException { + final JWTAuthOptionsFactory jwtAuthOptionsFactory = new JWTAuthOptionsFactory(); + final Path enclavePublicKey = Files.createTempFile("enclave", "pub"); + Files.writeString(enclavePublicKey, "invalidDataNo---HeadersAndNotBase64"); + + assertThatThrownBy( + () -> jwtAuthOptionsFactory.createForExternalPublicKey(enclavePublicKey.toFile())) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Authentication RPC public key file format is invalid"); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java index 4d98cbec074..03ad8b58d62 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java @@ -75,7 +75,7 @@ public void before() throws URISyntaxException { websocketConfiguration.setPort(0); websocketConfiguration.setAuthenticationEnabled(true); websocketConfiguration.setAuthenticationCredentialsFile(authTomlPath); - websocketConfiguration.setHostsWhitelist(Collections.singleton("*")); + websocketConfiguration.setHostsWhitelist(Collections.singletonList("*")); final Map<String, JsonRpcMethod> websocketMethods = new WebSocketMethodsFactory( diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java index 8f62a42457f..c54c2f59e98 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTest.java @@ -60,7 +60,7 @@ public void before() { websocketConfiguration = WebSocketConfiguration.createDefault(); websocketConfiguration.setPort(0); - websocketConfiguration.setHostsWhitelist(Collections.singleton("*")); + websocketConfiguration.setHostsWhitelist(Collections.singletonList("*")); final Map<String, JsonRpcMethod> websocketMethods = new WebSocketMethodsFactory( diff --git a/ethereum/api/src/test/resources/authentication/jwt_public_key b/ethereum/api/src/test/resources/authentication/jwt_public_key new file mode 100644 index 00000000000..9107cf29702 --- /dev/null +++ b/ethereum/api/src/test/resources/authentication/jwt_public_key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6tMhjogMulRbYby7bCL +rFhukDnxvm4XR3KSXLKdLLQHHyouMOQaLac9M+/Z1KkIpqfZPjLfW2/yUg2IKx4T +dvFVzbVq17X6dq49ZS8jJtb8l2+Vius4d3LnpvxCOematRG9Acn+2qLwC+sK7RPY +OxEqKPU5LNBH1C0FfviazY5jkixBFICzIq/SyyRnGX+iIONnNsu0TlhWVLSlZbg5 +NYf4cAzu/1d5MgspyZwnRo468gqaak3wQzkmk69Z25L1N7TXZvk2b7rT7/ssFnt+ +//fKVpD6qkQ3OopD+7gOziAYUxChw6RUWekV+uRgNADQhaqV6wDdogBz77wTJedV +YwIDAQAB +-----END PUBLIC KEY-----