From 1e5c2de3a4aafb476070478b27a18caf9efc051b Mon Sep 17 00:00:00 2001 From: Thomas <> Date: Mon, 8 May 2023 17:24:41 +0200 Subject: feat(core): add in-line method to KeyStoreFactory The keystore type 'inline' can be used to build a keystore by using PEM encoded certificate and key files. Example: pkcs12:keystore?private=certs/key.pem&cert=certs/certificate.pem --- .../core/impl/credential/EaafKeyStoreFactory.java | 39 ++++- .../impl/credential/KeyStoreConfiguration.java | 7 +- .../credential/inline/InlineKeyStoreBuilder.java | 165 +++++++++++++++++++++ .../credential/inline/InlineKeyStoreParser.java | 149 +++++++++++++++++++ .../messages/eaaf_utils_message.properties | 5 + 5 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreBuilder.java create mode 100644 eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreParser.java (limited to 'eaaf_core_utils/src/main') diff --git a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/EaafKeyStoreFactory.java b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/EaafKeyStoreFactory.java index 623e9d2c..fc3fa19d 100644 --- a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/EaafKeyStoreFactory.java +++ b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/EaafKeyStoreFactory.java @@ -5,6 +5,9 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStore.LoadStoreParameter; @@ -39,6 +42,7 @@ import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.core.exceptions.EaafFactoryException; import at.gv.egiz.eaaf.core.impl.credential.KeyStoreConfiguration.KeyStoreType; import at.gv.egiz.eaaf.core.impl.credential.SymmetricKeyConfiguration.SymmetricKeyType; +import at.gv.egiz.eaaf.core.impl.credential.inline.InlineKeyStoreParser; import at.gv.egiz.eaaf.core.impl.data.Pair; import at.gv.egiz.eaaf.core.impl.utils.FileUtils; import at.gv.egiz.eaaf.core.impl.utils.KeyStoreUtils; @@ -64,6 +68,9 @@ public class EaafKeyStoreFactory { public static final String ERRORCODE_07 = "internal.keystore.07"; public static final String ERRORCODE_10 = "internal.keystore.10"; public static final String ERRORCODE_11 = "internal.keystore.11"; + public static final String ERRORCODE_12 = "internal.keystore.12"; + public static final String ERRORCODE_13 = "internal.keystore.13"; + public static final String ERRORCODE_14 = "internal.keystore.14"; public static final String ERRORCODE_KEY_00 = "internal.key.00"; @@ -142,6 +149,9 @@ public class EaafKeyStoreFactory { || KeyStoreType.JKS.equals(config.getKeyStoreType())) { return getKeyStoreFromFileSystem(config); + } else if (KeyStoreType.INLINE.equals(config.getKeyStoreType())) { + return getKeyStoreFromInlineConfiguration(config); + } else if (KeyStoreType.HSMFACADE.equals(config.getKeyStoreType())) { if (isHsmFacadeInitialized) { return getKeyStoreFromHsmFacade(config); @@ -338,6 +348,33 @@ public class EaafKeyStoreFactory { } } + @Nonnull + private Pair getKeyStoreFromInlineConfiguration(KeyStoreConfiguration config) + throws EaafConfigurationException { + try { + log.debug("Loading keystore from in-line configuration URL ... "); + return Pair.newInstance( + InlineKeyStoreParser.buildKeyStore( + new URL(null, + config.getSoftKeyStoreFilePath(), + new InlineKeyStoreParser()), + resourceLoader, + basicConfig.getConfigurationRootDirectory()), + null); + + } catch (MalformedURLException e) { + log.error("Inline KeyStore URL has no valid form.", e); + throw new EaafConfigurationException(ERRORCODE_13, + new Object[] { config.getSoftKeyStoreFilePath(), e.getMessage() }, e); + + } catch (IOException | GeneralSecurityException e) { + log.error("Inline KeyStore initialization FAILED with an generic error.", e); + throw new EaafConfigurationException(ERRORCODE_13, new Object[] { e.getMessage() }, e); + + } + + } + @Nonnull private Pair getKeyStoreFromFileSystem(KeyStoreConfiguration config) throws EaafConfigurationException, EaafFactoryException { @@ -384,7 +421,7 @@ public class EaafKeyStoreFactory { } catch (final Exception e) { log.error("Software KeyStore initialization FAILED with an generic error.", e); - throw new EaafConfigurationException(ERRORCODE_03, new Object[] { e.getMessage() }, e); + throw new EaafConfigurationException(ERRORCODE_12, new Object[] { e.getMessage() }, e); } } diff --git a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/KeyStoreConfiguration.java b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/KeyStoreConfiguration.java index c1a1d917..7e66ca86 100644 --- a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/KeyStoreConfiguration.java +++ b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/KeyStoreConfiguration.java @@ -154,6 +154,11 @@ public class KeyStoreConfiguration { checkConfigurationValue(keyStoreName, EaafKeyStoreFactory.ERRORCODE_07, friendlyName, "Missing 'KeyName' for HSM-Facade"); + } else if (KeyStoreType.INLINE.equals(keyStoreType)) { + log.trace("Validate in-line KeyStore ... "); + checkConfigurationValue(softKeyStoreFilePath, EaafKeyStoreFactory.ERRORCODE_07, + friendlyName, "Missing 'KeyPath' for in-line keystore"); + } else if (KeyStoreType.PKCS12.equals(keyStoreType) || KeyStoreType.JKS.equals(keyStoreType)) { log.trace("Validate software KeyStore ... "); @@ -169,7 +174,7 @@ public class KeyStoreConfiguration { } public enum KeyStoreType { - PKCS12("pkcs12"), JKS("jks"), HSMFACADE("hsmfacade"), PKCS11("pkcs11"); + PKCS12("pkcs12"), JKS("jks"), HSMFACADE("hsmfacade"), PKCS11("pkcs11"), INLINE("inline"); private final String keyStoreType; diff --git a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreBuilder.java b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreBuilder.java new file mode 100644 index 00000000..a1e3a824 --- /dev/null +++ b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreBuilder.java @@ -0,0 +1,165 @@ +package at.gv.egiz.eaaf.core.impl.credential.inline; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; +import at.gv.egiz.eaaf.core.impl.utils.FileUtils; +import lombok.extern.slf4j.Slf4j; + +/** + * Convenience class to load keys and certificates in PEM-format into a + * {@link KeyStore} by providing the file paths. + */ +@Slf4j +public class InlineKeyStoreBuilder { + private final KeyStore keyStore; + private final ResourceLoader loader; + private final URI configRootDir; + + /** + * Create a new instance. + * + * @param type the type of the KeyStore to be built, e.g. + * "PKCS12" + * @param configRootDirectory Root directory for configuration + * @param resourceLoader Spring ResourceLoader + * @throws GeneralSecurityException if the empty KeyStore object could not be + * created + * @throws IOException if the empty KeyStore object could not be + * created + */ + public InlineKeyStoreBuilder(String type, ResourceLoader resourceLoader, + URI configRootDirectory) + throws GeneralSecurityException, IOException { + loader = resourceLoader; + configRootDir = configRootDirectory; + + keyStore = KeyStore.getInstance(type); + keyStore.load(null, null); + + } + + /** + * Sets a key entry, i.e. build a key store. + * + * @param privateKeyFile the path to the private key file + * @param certificateFiles the paths to the certificate files + * @throws GeneralSecurityException if a file could not be parsed + * @throws IOException if a file could not be read + * @throws EaafConfigurationException if a file could not be found + */ + public void setKeyEntry(String privateKeyFile, String[] certificateFiles) + throws GeneralSecurityException, IOException, EaafConfigurationException { + final X509Certificate[] certificates = readCertificates(certificateFiles); + final PrivateKey privateKey = readPrivateKey(privateKeyFile); + keyStore.setKeyEntry("keyEntry", privateKey, new char[0], certificates); + + } + + /** + * Sets certificate entries, i.e. build a trust store. + * + * @param certificateFiles the path to the certificate files + * @throws GeneralSecurityException if a file could not be parsed + */ + public void setCertificateEntries(String[] certificateFiles) throws GeneralSecurityException { + + final X509Certificate[] certificates = readCertificates(certificateFiles); + + for (int i = 0; i < certificates.length; i++) { + keyStore.setCertificateEntry("certificateEntry" + i, certificates[i]); + } + } + + /** + * Sets a secret key, i.e. builds a keystore. + * + * @param secret the secret encoded as a Base64 string + * @throws KeyStoreException if the secret key could not be added to the store + */ + public void setSecretEntry(final String secret) throws KeyStoreException { + final byte[] decodedKey = Base64.getDecoder().decode(secret); + final SecretKey key = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); + keyStore.setEntry("secret", new KeyStore.SecretKeyEntry(key), + new KeyStore.PasswordProtection(new char[] {})); + } + + public KeyStore getKeyStore() { + return keyStore; + } + + private X509Certificate[] readCertificates(String[] certificateFiles) throws CertificateException { + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + return Arrays.stream(certificateFiles).map(certificateFile -> { + try (InputStream is = readResourceFromFile(certificateFile)) { + return (X509Certificate) certificateFactory.generateCertificate(is); + + } catch (CertificateException | IOException | EaafConfigurationException e) { + log.error("Failed to load certificate file {}.", certificateFile, e); + return null; + + } + }).filter(Objects::nonNull).toArray(X509Certificate[]::new); + } + + private PrivateKey readPrivateKey(String filePath) throws IOException, EaafConfigurationException { + try ( + Reader fileReader = new InputStreamReader(readResourceFromFile(filePath)); + PEMParser pemParser = new PEMParser(fileReader)) { + + final Object object = pemParser.readObject(); + final JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + + if (object instanceof PrivateKeyInfo) { + return converter.getPrivateKey((PrivateKeyInfo) object); + + } else if (object instanceof PEMKeyPair) { + return converter.getKeyPair((PEMKeyPair) object).getPrivate(); + + } else { + throw new IllegalArgumentException("Unsupported object: " + object.getClass()); + + } + } + } + + private InputStream readResourceFromFile(String filePath) throws IOException, EaafConfigurationException { + final String absKeyStorePath = FileUtils.makeAbsoluteUrl(filePath, configRootDir); + log.debug("Use filepath from config: {}", absKeyStorePath); + Resource ressource = loader.getResource(absKeyStorePath); + + if (!ressource.exists()) { + throw new EaafConfigurationException("internal.keystore.15", + new Object[] { "RessourceLoader does NOT find File at: " + ressource.getURI() }); + + } + + return ressource.getInputStream(); + + } +} diff --git a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreParser.java b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreParser.java new file mode 100644 index 00000000..0ddc2680 --- /dev/null +++ b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/credential/inline/InlineKeyStoreParser.java @@ -0,0 +1,149 @@ +package at.gv.egiz.eaaf.core.impl.credential.inline; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.net.URLStreamHandler; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.core.io.ResourceLoader; + +import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; +import lombok.extern.slf4j.Slf4j; + +/** + * Parse in-line keystore configuration in BRZ format. + * + * @author tlenz + * + */ +@Slf4j +public class InlineKeyStoreParser extends URLStreamHandler { + + @Override + protected URLConnection openConnection(URL u) throws IOException { + log.error("openConnection is not implemented by {}", InlineKeyStoreParser.class); + return null; + + } + + /** + * Build a {@link KeyStore} object from an in-line configuration {@link URL}. + * + * @param configUrl configuration URL + * @param configRootDirectory Root directory for configuration + * @param resourceLoader Spring ResourceLoader + * @return KeyStore created by configuration URL + * @throws IOException In case of a configuration read error + * @throws GeneralSecurityException In case of a KeyStore configuration error + * @throws EaafConfigurationException If keyfile can not be found + */ + public static KeyStore buildKeyStore(URL configUrl, ResourceLoader resourceLoader, URI configRootDirectory) + throws IOException, GeneralSecurityException, EaafConfigurationException { + log.trace("Parsing keystore config from URL: {}", configUrl); + InlineKeyStoreBuilder keyStoreBuilder = new InlineKeyStoreBuilder( + configUrl.getProtocol(), resourceLoader, configRootDirectory); + + // parse basis properties from URL + final Map> queryParams = parseQuery(configUrl.getQuery()); + final String[] certificateFiles = queryParams.getOrDefault("cert", Collections.emptyList()) + .toArray(String[]::new); + + if ("keystore".equalsIgnoreCase(configUrl.getPath())) { + parseKeyStore(keyStoreBuilder, queryParams, certificateFiles); + + } else if ("truststore".equalsIgnoreCase(configUrl.getPath())) { + keyStoreBuilder.setCertificateEntries(certificateFiles); + + } else { + throw new IllegalArgumentException("Unknown store type: " + configUrl.getPath()); + + } + + return keyStoreBuilder.getKeyStore(); + } + + /** + * Takes the queryParams and builds based on them a {@link KeyStore} with + * secret and/or private key. + * + * @param keyStoreBuilder + * + * @param queryParams a map of all query params + * @param certificateFiles list of certificates + * @throws GeneralSecurityException if private or secret key cannot be parsed + * @throws IOException if private or secret key cannot be parsed + * @throws EaafConfigurationException if keyfile can not be found + */ + private static void parseKeyStore(InlineKeyStoreBuilder keyStoreBuilder, + final Map> queryParams, final String[] certificateFiles) + throws GeneralSecurityException, IOException, EaafConfigurationException { + final List privateKeyList = queryParams.get("private"); + final List secretKeyList = queryParams.get("inlineSecret"); + + if (privateKeyList == null && secretKeyList == null) { + throw new IllegalArgumentException("Neither secret key nor private key are configured!"); + + } + + if (privateKeyList != null) { + if (privateKeyList.size() == 1) { + final String privateKeyFile = queryParams.get("private").get(0); + keyStoreBuilder.setKeyEntry(privateKeyFile, certificateFiles); + + } else { + throw new IllegalArgumentException("Exactly one private key must be specified!"); + + } + } + + if (secretKeyList != null) { + if (secretKeyList.size() == 1) { + final String secret = URLDecoder.decode(secretKeyList.get(0), StandardCharsets.UTF_8); + keyStoreBuilder.setSecretEntry(secret); + + } else { + throw new IllegalArgumentException("Exactly one secret key must be specified!"); + + } + } + } + + private static Map> parseQuery(String query) { + if (query != null) { + final Map> queryParams = Arrays + .stream(query.split("&")) // generate a stream of key=value pairs + .map(keyValuePair -> keyValuePair.split("=")) // map key=value strings to arrays + .map(keyValuePair -> new AbstractMap.SimpleEntry(keyValuePair[0], + keyValuePair[1])) // map arrays to Entry objects + .collect(Collectors + .groupingBy(AbstractMap.SimpleEntry::getKey)) // group entries with identical keys to a map of + // lists + .entrySet().stream() // stream the map again + .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), + entry.getValue().stream().map(AbstractMap.SimpleEntry::getValue).collect(Collectors + .toList()))) // map the lists of Entry objects to lists of Strings + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, + AbstractMap.SimpleEntry::getValue)); // collect the stream to a map + + log.trace("Query map: {}", queryParams); + return queryParams; + + } else { + log.warn("Empty query string"); + return Collections.emptyMap(); + + } + } + +} diff --git a/eaaf_core_utils/src/main/resources/messages/eaaf_utils_message.properties b/eaaf_core_utils/src/main/resources/messages/eaaf_utils_message.properties index 79f82af8..2cc4e22e 100644 --- a/eaaf_core_utils/src/main/resources/messages/eaaf_utils_message.properties +++ b/eaaf_core_utils/src/main/resources/messages/eaaf_utils_message.properties @@ -13,6 +13,11 @@ internal.keystore.08=Can not access Key: {1} in KeyStore: {0} internal.keystore.09=Can not access Key: {1} in KeyStore: {0} Reason: {2} internal.keystore.10=HSM-Facade NOT INITIALIZED. Find HSM-Facade class: {0} put that looks WRONG. internal.keystore.11=KeyStore: {0} has a wrong configuration. Property: {0} Reason:{1} +internal.keystore.12=Software KeyStore initialization failed with a generic error: {0} +internal.keystore.13=Can not build KeyStore from in-line configuration. Reason: {0} +internal.keystore.14=KeyStore with in-line configuration: {0} is not a valid URL. Reason: {1} +internal.keystore.15=KeyStore with in-line configuration has an unknown query-parameter path. Reason: {0} + internal.key.00=Can not generate passphrase based symmetric-key: {0} Reason: {1} internal.key.01=Can not use key from Keystore: {0} Reason: {1} -- cgit v1.2.3