Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use temporary self signed certificates for JSON-RPC HTTP TLS unit testing #311

Merged
merged 16 commits into from
Jan 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ethereum/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ dependencies {
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.mockito:mockito-core'

testSupportImplementation 'org.bouncycastle:bcpkix-jdk15on'

integrationTestImplementation project(':config')
integrationTestImplementation project(path: ':config', configuration: 'testSupportArtifacts')
integrationTestImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
Expand All @@ -89,6 +91,10 @@ dependencies {
integrationTestImplementation 'org.mockito:mockito-core'
}

artifacts {
testSupportArtifacts testSupportJar
}

task generateTestBlockchain() {
def srcFiles = 'src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/trace/chain-data'
def dataPath = "$buildDir/generated/data"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
*
* 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.tls;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class KnownClientFileUtil {

public static void writeToKnownClientsFile(
final String commonName, final String fingerprint, final Path knownClientsFile) {
try {
final String knownClientsLine = String.format("%s %s", commonName, fingerprint);
Files.write(knownClientsFile, List.of("#Known Clients File", knownClientsLine));
} catch (final IOException e) {
throw new RuntimeException("Error in updating known clients file", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
*
* 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.tls;

import static org.hyperledger.besu.crypto.SecureRandomProvider.createSecureRandom;

import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.time.Instant;
import java.time.Period;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.tuweni.net.tls.TLS;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

/**
* Utility class to generate temporary self-signed certificates in PKCS12 format for testing
* purposes using BouncyCastle APIs. The generated certificate supports SAN extension for multiple
* DNS and IP addresses
*
* <p>Note: DO NOT USE IN PRODUCTION. The generated stores and files are marked to be deleted on JVM
* exit.
usmansaleem marked this conversation as resolved.
Show resolved Hide resolved
*/
public final class SelfSignedP12Certificate {
private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
private static final String alias = "test";
private static final boolean IS_CA = true;
private static final String distinguishedName = "CN=localhost";
private static final List<String> sanHostNames = List.of("localhost");
private static final List<String> sanIpAddresses = List.of("127.0.0.1");
private static final char[] password = "changeit".toCharArray();
private final Certificate certificate;
private final Path keyStore;
private final Path trustStore;

private SelfSignedP12Certificate(
final Certificate certificate, final Path keyStore, final Path trustStore) {
this.certificate = certificate;
this.keyStore = keyStore;
this.trustStore = trustStore;
}

public static SelfSignedP12Certificate create() {
try {
final KeyPair keyPair = generateKeyPair();
final Certificate certificate = generateSelfSignedCertificate(keyPair);
final Path keyStore = createKeyStore(keyPair.getPrivate(), certificate);
final Path trustStore = createTrustStore(certificate);
return new SelfSignedP12Certificate(certificate, keyStore, trustStore);
} catch (final IOException | GeneralSecurityException | OperatorCreationException e) {
throw new RuntimeException("Error creating self signed certificates", e);
}
}

public Certificate getCertificate() {
return certificate;
}

public Path getKeyStoreFile() {
return keyStore;
}

public Path getTrustStoreFile() {
return trustStore;
}

public char[] getPassword() {
return password;
}

public String getCommonName() {
try {
final X500Name subject = new X509CertificateHolder(certificate.getEncoded()).getSubject();
final RDN commonNameRdn = subject.getRDNs(BCStyle.CN)[0];
return IETFUtils.valueToString(commonNameRdn.getFirst().getValue());
} catch (final IOException | CertificateEncodingException e) {
throw new RuntimeException("Error extracting common name from certificate", e);
}
}

public String getCertificateHexFingerprint() {
try {
return TLS.certificateHexFingerprint(certificate);
} catch (CertificateEncodingException e) {
throw new RuntimeException("Error extracting certificate fingerprint", e);
}
}

private static KeyPair generateKeyPair() throws GeneralSecurityException {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048, createSecureRandom());
return keyPairGenerator.generateKeyPair();
}

private static Certificate generateSelfSignedCertificate(final KeyPair keyPair)
throws CertIOException, GeneralSecurityException, OperatorCreationException {
final X500Name issuer = new X500Name(distinguishedName);
final X500Name subject = new X500Name(distinguishedName);
final BigInteger serialNumber = new BigInteger(String.valueOf(Instant.now().toEpochMilli()));
final X509v3CertificateBuilder v3CertificateBuilder =
new JcaX509v3CertificateBuilder(
issuer,
serialNumber,
Date.from(Instant.now()),
Date.from(Instant.now().plus(Period.ofDays(90))),
subject,
keyPair.getPublic());

// extensions
v3CertificateBuilder.addExtension(
Extension.basicConstraints, true, new BasicConstraints(IS_CA));
v3CertificateBuilder.addExtension(
Extension.subjectAlternativeName, false, getSubjectAlternativeNames());

final ContentSigner contentSigner =
new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate());

return new JcaX509CertificateConverter()
.setProvider(BOUNCY_CASTLE_PROVIDER)
.getCertificate(v3CertificateBuilder.build(contentSigner));
}

private static GeneralNames getSubjectAlternativeNames() {
final List<GeneralName> hostGeneralNames =
sanHostNames.stream()
.map(hostName -> new GeneralName(GeneralName.dNSName, hostName))
.collect(Collectors.toList());
final List<GeneralName> ipGeneralNames =
sanIpAddresses.stream()
.map(ipAddress -> new GeneralName(GeneralName.iPAddress, ipAddress))
.collect(Collectors.toList());
final GeneralName[] generalNames =
Stream.of(hostGeneralNames, ipGeneralNames)
.flatMap(Collection::stream)
.toArray(GeneralName[]::new);

return new GeneralNames(generalNames);
}

private static Path createKeyStore(final PrivateKey privateKey, final Certificate certificate)
throws IOException, GeneralSecurityException {
final KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null);
keyStore.setKeyEntry(alias, privateKey, password, new Certificate[] {certificate});
return saveStore(keyStore);
}

private static Path createTrustStore(final Certificate certificate)
throws IOException, GeneralSecurityException {
final KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null);
keyStore.setCertificateEntry(alias, certificate);
return saveStore(keyStore);
}

private static Path saveStore(final KeyStore keyStore)
throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
final Path pfxPath = Files.createTempFile(alias, ".pfx");
pfxPath.toFile().deleteOnExit();
try (final FileOutputStream outputStream = new FileOutputStream(pfxPath.toFile())) {
keyStore.store(outputStream, password);
}
return pfxPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
*/
package org.hyperledger.besu.ethereum.api.jsonrpc;

import static com.google.common.io.Resources.getResource;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.NET;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3;
import static org.hyperledger.besu.ethereum.api.tls.KnownClientFileUtil.writeToKnownClientsFile;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;

Expand All @@ -30,6 +30,7 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider;
import org.hyperledger.besu.ethereum.api.tls.SelfSignedP12Certificate;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import org.hyperledger.besu.ethereum.blockcreation.EthHashMiningCoordinator;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
Expand All @@ -50,7 +51,6 @@
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
Expand All @@ -76,15 +76,19 @@ public class JsonRpcHttpServiceTlsMisconfigurationTest {
private static final String CLIENT_VERSION = "TestClientVersion/0.1.0";
private static final BigInteger CHAIN_ID = BigInteger.valueOf(123);
private static final Collection<RpcApi> JSON_RPC_APIS = List.of(ETH, NET, WEB3);
private static final String KEYSTORE_RESOURCE = "JsonRpcHttpService/rpc_keystore.pfx";
private static final String KNOWN_CLIENTS_RESOURCE = "JsonRpcHttpService/rpc_known_clients.txt";
private static final NatService natService = new NatService(Optional.empty());

private final SelfSignedP12Certificate besuCertificate = SelfSignedP12Certificate.create();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is besuCertificate correct here, or is this the certificate presented by the client?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

besuCertificate contains certificate keystore used by Besu. It also exposes trustStore that can be used by OkHttp client library to trust Besu.

private Path knownClientsFile;
private Map<String, JsonRpcMethod> rpcMethods;
private JsonRpcHttpService service;

@Before
public void beforeEach() {
public void beforeEach() throws IOException {
knownClientsFile = folder.newFile().toPath();
writeToKnownClientsFile(
besuCertificate.getCommonName(),
besuCertificate.getCertificateHexFingerprint(),
knownClientsFile);
final P2PNetwork peerDiscoveryMock = mock(P2PNetwork.class);
final BlockchainQueries blockchainQueries = mock(BlockchainQueries.class);
final Synchronizer synchronizer = mock(Synchronizer.class);
Expand Down Expand Up @@ -202,29 +206,31 @@ public void exceptionRaisedWhenInvalidKnownClientsFileIsSpecified() throws IOExc

private TlsConfiguration invalidKeystoreFileTlsConfiguration() throws IOException {
final File tempFile = folder.newFile();
return new TlsConfiguration(tempFile.toPath(), () -> "invalid_password", getKnownClientsFile());
return new TlsConfiguration(tempFile.toPath(), () -> "invalid_password", knownClientsFile);
}

private TlsConfiguration invalidKeystorePathTlsConfiguration() {
return new TlsConfiguration(
Path.of("/tmp/invalidkeystore.pfx"), () -> "invalid_password", getKnownClientsFile());
Path.of("/tmp/invalidkeystore.pfx"), () -> "invalid_password", knownClientsFile);
}

private TlsConfiguration invalidPasswordTlsConfiguration() {
return new TlsConfiguration(getKeyStorePath(), () -> "invalid_password", getKnownClientsFile());
return new TlsConfiguration(
besuCertificate.getKeyStoreFile(), () -> "invalid_password", knownClientsFile);
}

private TlsConfiguration invalidPasswordFileTlsConfiguration() {
return new TlsConfiguration(
getKeyStorePath(),
besuCertificate.getKeyStoreFile(),
new FileBasedPasswordProvider(Path.of("/tmp/invalid_password_file.txt")),
getKnownClientsFile());
knownClientsFile);
}

private TlsConfiguration invalidKnownClientsTlsConfiguration() throws IOException {
final Path tempKnownClientsFile = folder.newFile().toPath();
Files.write(tempKnownClientsFile, List.of("cn invalid_sha256"));
return new TlsConfiguration(getKeyStorePath(), () -> "changeit", tempKnownClientsFile);
return new TlsConfiguration(
besuCertificate.getKeyStoreFile(), () -> "changeit", tempKnownClientsFile);
}

private JsonRpcHttpService createJsonRpcHttpService(
Expand All @@ -249,12 +255,4 @@ private JsonRpcConfiguration createJsonRpcConfig(
config.setTlsConfiguration(tlsConfigurationSupplier);
return config;
}

private static Path getKeyStorePath() {
return Paths.get(getResource(KEYSTORE_RESOURCE).getPath());
}

private static Path getKnownClientsFile() {
return Paths.get(getResource(KNOWN_CLIENTS_RESOURCE).getPath());
}
}
Loading