diff --git a/besu/build.gradle b/besu/build.gradle index fb6a64e9e9..02045acd83 100644 --- a/besu/build.gradle +++ b/besu/build.gradle @@ -29,6 +29,7 @@ jar { dependencies { implementation project(':config') + implementation project(':pki') implementation project(':consensus:clique') implementation project(':consensus:common') implementation project(':consensus:ibft') diff --git a/pki/build.gradle b/pki/build.gradle new file mode 100644 index 0000000000..f0e0e2aaaf --- /dev/null +++ b/pki/build.gradle @@ -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 + */ + +apply plugin: 'java-library' + +jar { + archiveBaseName = 'besu-pki' + manifest { + attributes( + 'Specification-Title': archiveBaseName, + 'Specification-Version': project.version, + 'Implementation-Title': archiveBaseName, + 'Implementation-Version': calculateVersion() + ) + } +} + +dependencies { + implementation 'com.google.guava:guava' + implementation 'org.apache.logging.log4j:log4j-api' + implementation 'org.apache.tuweni:bytes' + implementation 'org.bouncycastle:bcpkix-jdk15on' + + testImplementation 'junit:junit' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/PkiConfiguration.java b/pki/src/main/java/org/hyperledger/besu/pki/PkiConfiguration.java new file mode 100644 index 0000000000..5b107fda2e --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/PkiConfiguration.java @@ -0,0 +1,141 @@ +/* + * 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.pki; + +import static java.util.Objects.requireNonNull; + +import java.nio.file.Path; +import java.util.function.Supplier; + +public class PkiConfiguration { + + public static String DEFAULT_KEYSTORE_TYPE = "PKCS12"; + public static String DEFAULT_CERTIFICATE_ALIAS = "validator"; + + private final String keyStoreType; + private final Path keyStorePath; + private final Supplier keyStorePasswordSupplier; + private final String certificateAlias; + private final String trustStoreType; + private final Path trustStorePath; + private final Supplier trustStorePasswordSupplier; + + public PkiConfiguration( + final String keyStoreType, + final Path keyStorePath, + final Supplier keyStorePasswordSupplier, + final String certificateAlias, + final String trustStoreType, + final Path trustStorePath, + final Supplier trustStorePasswordSupplier) { + this.keyStoreType = keyStoreType; + this.keyStorePath = keyStorePath; + this.keyStorePasswordSupplier = keyStorePasswordSupplier; + this.certificateAlias = certificateAlias; + this.trustStoreType = trustStoreType; + this.trustStorePath = trustStorePath; + this.trustStorePasswordSupplier = trustStorePasswordSupplier; + } + + public String getKeyStoreType() { + return keyStoreType; + } + + public Path getKeyStorePath() { + return keyStorePath; + } + + public String getKeyStorePassword() { + return null == keyStorePasswordSupplier ? null : keyStorePasswordSupplier.get(); + } + + public String getCertificateAlias() { + return certificateAlias; + } + + public String getTrustStoreType() { + return trustStoreType; + } + + public Path getTrustStorePath() { + return trustStorePath; + } + + public String getTrustStorePassword() { + return trustStorePasswordSupplier.get(); + } + + public static final class Builder { + + private String keyStoreType = DEFAULT_KEYSTORE_TYPE; + private Path keyStorePath; + private Supplier keyStorePasswordSupplier; + private String certificateAlias = DEFAULT_CERTIFICATE_ALIAS; + private String trustStoreType = DEFAULT_KEYSTORE_TYPE; + private Path trustStorePath; + private Supplier trustStorePasswordSupplier; + + public Builder() {} + + public Builder withKeyStoreType(final String keyStoreType) { + this.keyStoreType = keyStoreType; + return this; + } + + public Builder withKeyStorePath(final Path keyStorePath) { + this.keyStorePath = keyStorePath; + return this; + } + + public Builder withKeyStorePasswordSupplier(final Supplier keyStorePasswordSupplier) { + this.keyStorePasswordSupplier = keyStorePasswordSupplier; + return this; + } + + public Builder withCertificateAlias(final String certificateAlias) { + this.certificateAlias = certificateAlias; + return this; + } + + public Builder withTrustStoreType(final String trustStoreType) { + this.trustStoreType = trustStoreType; + return this; + } + + public Builder withTrustStorePath(final Path trustStorePath) { + this.trustStorePath = trustStorePath; + return this; + } + + public Builder withTrustStorePasswordSupplier( + final Supplier trustStorePasswordSupplier) { + this.trustStorePasswordSupplier = trustStorePasswordSupplier; + return this; + } + + public PkiConfiguration build() { + requireNonNull(keyStoreType, "Key Store Type must not be null"); + requireNonNull(keyStorePasswordSupplier, "Key Store password supplier must not be null"); + return new PkiConfiguration( + keyStoreType, + keyStorePath, + keyStorePasswordSupplier, + certificateAlias, + trustStoreType, + trustStorePath, + trustStorePasswordSupplier); + } + } +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/PkiException.java b/pki/src/main/java/org/hyperledger/besu/pki/PkiException.java new file mode 100644 index 0000000000..22d4baa9c0 --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/PkiException.java @@ -0,0 +1,36 @@ +/* + * 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.pki; + +public class PkiException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public PkiException() { + super(); + } + + public PkiException(final String message) { + super(message); + } + + public PkiException(final String message, final Throwable t) { + super(message, t); + } + + public PkiException(final Throwable t) { + super(t); + } +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/keystore/HardwareKeyStoreWrapper.java b/pki/src/main/java/org/hyperledger/besu/pki/keystore/HardwareKeyStoreWrapper.java new file mode 100644 index 0000000000..52f55a4ee6 --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/keystore/HardwareKeyStoreWrapper.java @@ -0,0 +1,170 @@ +/* + * 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.pki.keystore; + +import org.hyperledger.besu.pki.PkiException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Creates an instance of this class which is backed by a PKCS#11 keystore, such as a software + * (emulated) HSM or a physical/cloud HSM (see here + */ +public class HardwareKeyStoreWrapper implements KeyStoreWrapper { + + private static final Logger LOG = LogManager.getLogger(); + + private static final String pkcs11Provider = "SunPKCS11"; + + private final KeyStore keystore; + private final transient char[] keystorePassword; + + private final java.security.Provider provider; + + public HardwareKeyStoreWrapper(final String keystorePassword, final Provider provider) { + try { + if (provider == null) { + throw new IllegalArgumentException("Provider is null"); + } + this.keystorePassword = keystorePassword.toCharArray(); + + this.provider = provider; + if (Security.getProvider(provider.getName()) == null) { + Security.addProvider(provider); + } + + keystore = KeyStore.getInstance(KeyStoreWrapper.KEYSTORE_TYPE_PKCS11, provider); + keystore.load(null, this.keystorePassword); + + } catch (final Exception e) { + throw new PkiException("Failed to initialize HSM keystore", e); + } + } + + public HardwareKeyStoreWrapper(final String keystorePassword, final Path config) { + try { + if (keystorePassword == null) { + throw new IllegalArgumentException("Keystore password is null"); + } + final Properties properties = new Properties(); + final File configFile = config.toFile(); + try (InputStream ins = new FileInputStream(configFile)) { + properties.load(ins); + } + final String name = properties.getProperty("name"); + this.keystorePassword = keystorePassword.toCharArray(); + final Optional existingProvider = + Stream.of(Security.getProviders()) + .filter(p -> p.getName().equals(String.format("%s-%s", pkcs11Provider, name))) + .findAny(); + if (existingProvider.isPresent()) { + provider = existingProvider.get(); + } else { + provider = getPkcs11Provider(configFile.getAbsolutePath()); + Security.addProvider(provider); + } + + keystore = KeyStore.getInstance(KeyStoreWrapper.KEYSTORE_TYPE_PKCS11, provider); + keystore.load(null, this.keystorePassword); + + } catch (final Exception e) { + throw new PkiException("Failed to initialize HSM keystore", e); + } + } + + @VisibleForTesting + HardwareKeyStoreWrapper(final KeyStore keystore, final String password) { + this.keystore = keystore; + this.keystorePassword = password.toCharArray(); + this.provider = null; + } + + @Override + public PrivateKey getPrivateKey(final String keyAlias) { + try { + LOG.debug("Retrieving private key for alias: {}", keyAlias); + return (PrivateKey) keystore.getKey(keyAlias, this.keystorePassword); + } catch (final Exception e) { + throw new PkiException("Failed to get key: " + keyAlias, e); + } + } + + @Override + public PublicKey getPublicKey(final String keyAlias) { + try { + LOG.debug("Retrieving public key for alias: {}", keyAlias); + final Certificate certificate = keystore.getCertificate(keyAlias); + return (certificate != null) ? certificate.getPublicKey() : null; + } catch (final Exception e) { + throw new PkiException("Failed to get key: " + keyAlias, e); + } + } + + @Override + public Certificate getCertificate(final String certificateAlias) { + try { + LOG.debug("Retrieving certificate for alias: {}", certificateAlias); + return keystore.getCertificate(certificateAlias); + } catch (final Exception e) { + throw new PkiException("Failed to get certificate: " + certificateAlias, e); + } + } + + @Override + public Certificate[] getCertificateChain(final String certificateAlias) { + try { + LOG.debug("Retrieving certificate chain for alias: {}", certificateAlias); + return keystore.getCertificateChain(certificateAlias); + } catch (final Exception e) { + throw new PkiException("Failed to certificate chain for alias: " + certificateAlias, e); + } + } + + @Override + public KeyStore getKeyStore() { + return keystore; + } + + @Override + public KeyStore getTrustStore() { + return keystore; + } + + private Provider getPkcs11Provider(final String config) { + final Provider provider = Security.getProvider(pkcs11Provider); + if (null == provider) { + throw new IllegalArgumentException("Unable to load PKCS11 provider configuration."); + } else { + return provider.configure(config); + } + } +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/keystore/KeyStoreWrapper.java b/pki/src/main/java/org/hyperledger/besu/pki/keystore/KeyStoreWrapper.java new file mode 100644 index 0000000000..b1decb4d1b --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/keystore/KeyStoreWrapper.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.pki.keystore; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; + +public interface KeyStoreWrapper { + + String KEYSTORE_TYPE_JKS = "JKS"; + String KEYSTORE_TYPE_PKCS11 = "PKCS11"; + String KEYSTORE_TYPE_PKCS12 = "PKCS12"; + + KeyStore getKeyStore(); + + KeyStore getTrustStore(); + + PrivateKey getPrivateKey(String keyAlias); + + PublicKey getPublicKey(String keyAlias); + + Certificate getCertificate(String certificateAlias); + + Certificate[] getCertificateChain(String certificateAlias); +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapper.java b/pki/src/main/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapper.java new file mode 100644 index 0000000000..9a2d4e3c25 --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapper.java @@ -0,0 +1,215 @@ +/* + * 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.pki.keystore; + +import org.hyperledger.besu.pki.PkiException; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.util.HashMap; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SoftwareKeyStoreWrapper implements KeyStoreWrapper { + + private static final Logger LOG = LogManager.getLogger(); + + private final KeyStore keystore; + private final transient char[] keystorePassword; + private KeyStore truststore; + private transient char[] truststorePassword; + + private final Map cachedPrivateKeys = new HashMap<>(); + private final Map cachedPublicKeys = new HashMap<>(); + private final Map cachedCertificates = new HashMap<>(); + + public SoftwareKeyStoreWrapper( + final String keystoreType, final Path keystoreLocation, final String keystorePassword) { + this(keystoreType, keystoreLocation, keystorePassword, null, null, null); + } + + public SoftwareKeyStoreWrapper( + final String keystoreType, + final Path keystoreLocation, + final String keystorePassword, + final String truststoreType, + final Path truststoreLocation, + final String truststorePassword) { + + if (keystorePassword == null) { + throw new IllegalArgumentException("Keystore password is null"); + } + this.keystorePassword = keystorePassword.toCharArray(); + try (InputStream stream = new FileInputStream(keystoreLocation.toFile())) { + keystore = KeyStore.getInstance(keystoreType); + keystore.load(stream, this.keystorePassword); + + } catch (final Exception e) { + throw new PkiException("Failed to initialize software keystore: " + keystoreLocation, e); + } + + if (truststoreType != null && truststoreLocation != null) { + this.truststorePassword = + (truststorePassword != null) ? truststorePassword.toCharArray() : null; + try (InputStream stream = new FileInputStream(truststoreLocation.toFile())) { + truststore = KeyStore.getInstance(truststoreType); + truststore.load(stream, this.truststorePassword); + + } catch (final Exception e) { + throw new PkiException( + "Failed to initialize software truststore: " + truststoreLocation, e); + } + } + } + + @VisibleForTesting + SoftwareKeyStoreWrapper( + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final String truststorePassword) { + this.keystore = keystore; + this.keystorePassword = keystorePassword.toCharArray(); + this.truststore = truststore; + this.truststorePassword = truststorePassword.toCharArray(); + } + + @Override + public PrivateKey getPrivateKey(final String keyAlias) { + LOG.debug("Retrieving private key for alias: {}", keyAlias); + return (PrivateKey) getKey(keyAlias, PrivateKey.class, cachedPrivateKeys); + } + + @Override + public PublicKey getPublicKey(final String keyAlias) { + LOG.debug("Retrieving public key for alias: {}", keyAlias); + return (PublicKey) getKey(keyAlias, PublicKey.class, cachedPublicKeys); + } + + @Override + public Certificate getCertificate(final String certificateAlias) { + try { + LOG.debug("Retrieving certificate for alias: {}", certificateAlias); + Certificate certificate = cachedCertificates.get(certificateAlias); + if (certificate == null) { + LOG.debug("Certificate alias: {} not cached", certificateAlias); + + certificate = keystore.getCertificate(certificateAlias); + if (certificate == null && truststore != null) { + certificate = truststore.getCertificate(certificateAlias); + } + if (certificate != null) { + LOG.debug("Certificate alias: {} found in keystore/truststore", certificateAlias); + cachedCertificates.put(certificateAlias, certificate); + cachedPublicKeys.put(certificateAlias, certificate.getPublicKey()); + return certificate; + } else { + LOG.warn("Certificate alias: {} not found in keystore/truststore", certificateAlias); + } + } + return certificate; + + } catch (final Exception e) { + throw new PkiException("Failed to get certificate: " + certificateAlias, e); + } + } + + @Override + public Certificate[] getCertificateChain(final String certificateAlias) { + try { + LOG.debug("Retrieving certificate chain for alias: {}", certificateAlias); + + Certificate[] certificateChain = keystore.getCertificateChain(certificateAlias); + if (certificateChain == null && truststore != null) { + certificateChain = truststore.getCertificateChain(certificateAlias); + } + return certificateChain; + } catch (final Exception e) { + throw new PkiException("Failed to certificate chain for alias: " + certificateAlias, e); + } + } + + private Key getKey( + final String keyAlias, + final Class keyTypeClass, + final Map keyCache) { + Key cachedKey = keyCache.get(keyAlias); + if (cachedKey == null) { + LOG.debug("Key alias: {} not cached", keyAlias); + try { + cachedKey = loadAndCacheKey(this.keystore, this.keystorePassword, keyAlias, keyTypeClass); + if (cachedKey == null) { + cachedKey = + loadAndCacheKey(this.truststore, this.truststorePassword, keyAlias, keyTypeClass); + } + } catch (final Exception e) { + throw new PkiException("Failed to get key: " + keyAlias, e); + } + } + return cachedKey; + } + + @Override + public KeyStore getKeyStore() { + return keystore; + } + + @Override + public KeyStore getTrustStore() { + return truststore; + } + + private Key loadAndCacheKey( + final KeyStore keystore, + final char[] keystorePassword, + final String keyAlias, + final Class keyTypeClass) + throws GeneralSecurityException { + if (keystore != null && keystore.containsAlias(keyAlias)) { + + final Key key = keystore.getKey(keyAlias, keystorePassword); + if (key != null) { + LOG.debug("Key alias: {} found in keystore/truststore", keyAlias); + if (key instanceof PrivateKey && PrivateKey.class.isAssignableFrom(keyTypeClass)) { + cachedPrivateKeys.put(keyAlias, (PrivateKey) key); + return key; + } else if (key instanceof PublicKey && PublicKey.class.isAssignableFrom(keyTypeClass)) { + cachedPublicKeys.put(keyAlias, (PublicKey) key); + return key; + } + } + + if (PublicKey.class.isAssignableFrom(keyTypeClass)) { + final Certificate certificate = getCertificate(keyAlias); + if (certificate != null) { + return certificate.getPublicKey(); + } + } + } + + LOG.warn("Key alias: {} not found in keystore/truststore", keyAlias); + return null; + } +} diff --git a/pki/src/test/java/org/hyperledger/besu/pki/keystore/HardwareKeyStoreWrapperTest.java b/pki/src/test/java/org/hyperledger/besu/pki/keystore/HardwareKeyStoreWrapperTest.java new file mode 100644 index 0000000000..bf533c79fb --- /dev/null +++ b/pki/src/test/java/org/hyperledger/besu/pki/keystore/HardwareKeyStoreWrapperTest.java @@ -0,0 +1,81 @@ +/* + * 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.pki.keystore; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class HardwareKeyStoreWrapperTest { + + private static final String KEY_ALIAS = "keyalias"; + private static final String CERTIFICATE_ALIAS = "certalias"; + private static final char[] PASSWORD = "password".toCharArray(); + + @Mock private KeyStore keyStore; + @Mock private PrivateKey privateKey; + @Mock private PublicKey publicKey; + @Mock private Certificate certificate; + + private HardwareKeyStoreWrapper keyStoreWrapper; + + @Before + public void before() { + keyStoreWrapper = new HardwareKeyStoreWrapper(keyStore, new String(PASSWORD)); + } + + @Test + public void getPrivateKey() throws Exception { + when(keyStore.getKey(KEY_ALIAS, PASSWORD)).thenReturn(privateKey); + + assertThat(keyStoreWrapper.getPrivateKey(KEY_ALIAS)).isNotNull(); + } + + @Test + public void getPublicKey() throws Exception { + // Get public key from certificate + when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(certificate); + when(certificate.getPublicKey()).thenReturn(publicKey); + + assertThat(keyStoreWrapper.getPublicKey(KEY_ALIAS)).isNotNull(); + } + + @Test + public void getCertificate() throws Exception { + when(keyStore.getCertificate(CERTIFICATE_ALIAS)).thenReturn(certificate); + + assertThat(keyStoreWrapper.getCertificate(CERTIFICATE_ALIAS)).isNotNull(); + } + + @Test + public void getCertificateChain() throws Exception { + when(keyStore.getCertificateChain(CERTIFICATE_ALIAS)) + .thenReturn(new Certificate[] {certificate}); + + assertThat(keyStoreWrapper.getCertificateChain(CERTIFICATE_ALIAS)).hasSize(1); + } +} diff --git a/pki/src/test/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapperTest.java b/pki/src/test/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapperTest.java new file mode 100644 index 0000000000..80213cf3c6 --- /dev/null +++ b/pki/src/test/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapperTest.java @@ -0,0 +1,196 @@ +/* + * 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.pki.keystore; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.pki.keystore.KeyStoreWrapper.KEYSTORE_TYPE_PKCS12; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SoftwareKeyStoreWrapperTest { + + private static final String KEY_ALIAS = "keyalias"; + private static final String CERTIFICATE_ALIAS = "certalias"; + private static final char[] PASSWORD = "password".toCharArray(); + + private SoftwareKeyStoreWrapper keyStoreWrapper; + + @Mock private KeyStore keyStore; + @Mock private KeyStore trustStore; + @Mock private PrivateKey privateKey; + @Mock private PublicKey publicKey; + @Mock private Certificate certificate; + + @Before + public void before() { + keyStoreWrapper = new SoftwareKeyStoreWrapper(keyStore, new String(PASSWORD), null, ""); + } + + @Test + public void getPrivateKey() throws Exception { + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + when(keyStore.getKey(KEY_ALIAS, PASSWORD)).thenReturn(privateKey); + + assertThat(keyStoreWrapper.getPrivateKey(KEY_ALIAS)).isNotNull(); + } + + @Test + public void getPrivateKeyCaching() throws Exception { + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + when(keyStore.getKey(KEY_ALIAS, PASSWORD)).thenReturn(privateKey); + + keyStoreWrapper.getPrivateKey(KEY_ALIAS); + keyStoreWrapper.getPrivateKey(KEY_ALIAS); + + verify(keyStore, times(1)).getKey(eq(KEY_ALIAS), eq(PASSWORD)); + } + + @Test + public void getPrivateKeyFallbackToTrustStore() throws Exception { + keyStoreWrapper = + new SoftwareKeyStoreWrapper( + keyStore, new String(PASSWORD), trustStore, new String(PASSWORD)); + + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + when(trustStore.containsAlias(KEY_ALIAS)).thenReturn(true); + when(trustStore.getKey(KEY_ALIAS, PASSWORD)).thenReturn(privateKey); + + assertThat(keyStoreWrapper.getPrivateKey(KEY_ALIAS)).isNotNull(); + + verify(trustStore).getKey(eq(KEY_ALIAS), eq(PASSWORD)); + } + + @Test + public void getPublicKey() throws Exception { + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + when(keyStore.getKey(KEY_ALIAS, PASSWORD)).thenReturn(publicKey); + + assertThat(keyStoreWrapper.getPublicKey(KEY_ALIAS)).isNotNull(); + } + + @Test + public void getPublicKeyCaching() throws Exception { + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + when(keyStore.getKey(KEY_ALIAS, PASSWORD)).thenReturn(publicKey); + + keyStoreWrapper.getPublicKey(KEY_ALIAS); + keyStoreWrapper.getPublicKey(KEY_ALIAS); + + verify(keyStore, times(1)).getKey(eq(KEY_ALIAS), eq(PASSWORD)); + } + + @Test + public void getPublicKeyFallbackToTrustStore() throws Exception { + keyStoreWrapper = + new SoftwareKeyStoreWrapper( + keyStore, new String(PASSWORD), trustStore, new String(PASSWORD)); + + when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + when(trustStore.containsAlias(KEY_ALIAS)).thenReturn(true); + when(trustStore.getKey(KEY_ALIAS, PASSWORD)).thenReturn(publicKey); + + assertThat(keyStoreWrapper.getPublicKey(KEY_ALIAS)).isNotNull(); + + verify(trustStore).getKey(eq(KEY_ALIAS), eq(PASSWORD)); + } + + @Test + public void getCertificate() throws Exception { + when(keyStore.getCertificate(CERTIFICATE_ALIAS)).thenReturn(certificate); + + assertThat(keyStoreWrapper.getCertificate(CERTIFICATE_ALIAS)).isNotNull(); + } + + @Test + public void getCertificateCaching() throws Exception { + when(keyStore.getCertificate(CERTIFICATE_ALIAS)).thenReturn(certificate); + + keyStoreWrapper.getCertificate(CERTIFICATE_ALIAS); + keyStoreWrapper.getCertificate(CERTIFICATE_ALIAS); + + verify(keyStore, times(1)).getCertificate(eq(CERTIFICATE_ALIAS)); + } + + @Test + public void getCertificateFallbackToTrustStore() throws Exception { + keyStoreWrapper = + new SoftwareKeyStoreWrapper( + keyStore, new String(PASSWORD), trustStore, new String(PASSWORD)); + + when(keyStore.getCertificate(CERTIFICATE_ALIAS)).thenReturn(null); + when(trustStore.getCertificate(CERTIFICATE_ALIAS)).thenReturn(certificate); + + assertThat(keyStoreWrapper.getCertificate(CERTIFICATE_ALIAS)).isNotNull(); + + verify(trustStore).getCertificate(eq(CERTIFICATE_ALIAS)); + } + + @Test + public void getCertificateChain() throws Exception { + when(keyStore.getCertificateChain(CERTIFICATE_ALIAS)) + .thenReturn(new Certificate[] {certificate}); + + assertThat(keyStoreWrapper.getCertificateChain(CERTIFICATE_ALIAS)).hasSize(1); + } + + @Test + public void getCertificateChainFallbackToTrustStore() throws Exception { + keyStoreWrapper = + new SoftwareKeyStoreWrapper( + keyStore, new String(PASSWORD), trustStore, new String(PASSWORD)); + + when(keyStore.getCertificateChain(CERTIFICATE_ALIAS)).thenReturn(null); + when(trustStore.getCertificateChain(CERTIFICATE_ALIAS)) + .thenReturn(new Certificate[] {certificate}); + + assertThat(keyStoreWrapper.getCertificateChain(CERTIFICATE_ALIAS)).hasSize(1); + + verify(trustStore).getCertificateChain(eq(CERTIFICATE_ALIAS)); + } + + @Test + public void loadKeyStoreFromFile() { + SoftwareKeyStoreWrapper loadedKeyStore = + new SoftwareKeyStoreWrapper( + KEYSTORE_TYPE_PKCS12, + Path.of("src/test/resources/keystore/keystore"), + "validator", + KEYSTORE_TYPE_PKCS12, + Path.of("src/test/resources/keystore/keystore"), + "validator"); + + assertThat(loadedKeyStore.getPublicKey("validator")).isNotNull(); + assertThat(loadedKeyStore.getPrivateKey("validator")).isNotNull(); + assertThat(loadedKeyStore.getCertificate("validator")).isNotNull(); + // CA -> INTERCA -> PARTNERACA -> VALIDATOR + assertThat(loadedKeyStore.getCertificateChain("validator")).hasSize(4); + } +} diff --git a/pki/src/test/resources/keystore/keystore b/pki/src/test/resources/keystore/keystore new file mode 100644 index 0000000000..5ca6e77851 Binary files /dev/null and b/pki/src/test/resources/keystore/keystore differ diff --git a/pki/src/test/resources/keystore/truststore b/pki/src/test/resources/keystore/truststore new file mode 100644 index 0000000000..5a9fc09310 Binary files /dev/null and b/pki/src/test/resources/keystore/truststore differ diff --git a/pki/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/pki/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/pki/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 62d0ef3f77..fe5ad8ea21 100644 --- a/settings.gradle +++ b/settings.gradle @@ -45,6 +45,7 @@ include 'ethereum:trie' include 'metrics:core' include 'metrics:rocksdb' include 'nat' +include 'pki' include 'plugin-api' include 'plugins:rocksdb' include 'privacy-contracts'