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 +++++++++++++++++++
 4 files changed, 358 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/java/at')

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<KeyStore, Provider> 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<KeyStore, Provider> 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<String, List<String>> 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 <i>queryParams</i> 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<String, List<String>> queryParams, final String[] certificateFiles)
+      throws GeneralSecurityException, IOException, EaafConfigurationException {
+    final List<String> privateKeyList = queryParams.get("private");
+    final List<String> 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<String, List<String>> parseQuery(String query) {
+    if (query != null) {
+      final Map<String, List<String>> 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<String, String>(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();
+
+    }
+  }
+
+}
-- 
cgit v1.2.3