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 + .../test/credentials/EaafKeyStoreFactoryTest.java | 83 +++++++++++ .../core/test/credentials/InlineKeyStoreTest.java | 140 +++++++++++++++++ .../test/resources/data/certs/BRZStammCA201.pem | 33 +++++ .../resources/data/certs/invalidCertificate.pem | 19 +++ .../src/test/resources/data/certs/privateEcKey.pem | 5 + .../src/test/resources/data/certs/privateKey.pem | 28 ++++ .../resources/data/certs/selfSignedCertificate.pem | 19 +++ .../data/certs/selfSignedEcCertificate.pem | 11 ++ 13 files changed, 701 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 create mode 100644 eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/InlineKeyStoreTest.java create mode 100644 eaaf_core_utils/src/test/resources/data/certs/BRZStammCA201.pem create mode 100644 eaaf_core_utils/src/test/resources/data/certs/invalidCertificate.pem create mode 100644 eaaf_core_utils/src/test/resources/data/certs/privateEcKey.pem create mode 100644 eaaf_core_utils/src/test/resources/data/certs/privateKey.pem create mode 100644 eaaf_core_utils/src/test/resources/data/certs/selfSignedCertificate.pem create mode 100644 eaaf_core_utils/src/test/resources/data/certs/selfSignedEcCertificate.pem (limited to 'eaaf_core_utils') 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} diff --git a/eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/EaafKeyStoreFactoryTest.java b/eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/EaafKeyStoreFactoryTest.java index 3e82c510..932beb31 100644 --- a/eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/EaafKeyStoreFactoryTest.java +++ b/eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/EaafKeyStoreFactoryTest.java @@ -1,5 +1,8 @@ package at.gv.egiz.eaaf.core.test.credentials; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; @@ -383,6 +386,86 @@ public class EaafKeyStoreFactoryTest { } + @Test + @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD) + public void inlineKeyStoreMissingPath() throws EaafException { + final EaafKeyStoreFactory keyStoreFactory = context.getBean(EaafKeyStoreFactory.class); + Assert.assertFalse("HSM Facade state wrong", keyStoreFactory.isHsmFacadeInitialized()); + + final KeyStoreConfiguration keyStoreConfig = new KeyStoreConfiguration(); + keyStoreConfig.setKeyStoreType(KeyStoreType.INLINE); + + EaafConfigurationException error = assertThrows("wrong exception", EaafConfigurationException.class, + () -> keyStoreConfig.validate()); + assertEquals("internal.keystore.07", error.getErrorId(), "wrong errorcode"); + + } + + @Test + @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD) + public void inlineKeyStoreSuccess() throws EaafException { + final EaafKeyStoreFactory keyStoreFactory = context.getBean(EaafKeyStoreFactory.class); + Assert.assertFalse("HSM Facade state wrong", keyStoreFactory.isHsmFacadeInitialized()); + + final KeyStoreConfiguration keyStoreConfig = new KeyStoreConfiguration(); + keyStoreConfig.setKeyStoreType(KeyStoreType.INLINE); + keyStoreConfig.setSoftKeyStoreFilePath( + "pkcs12:keystore?private=src/test/resources/data/certs/privateKey.pem" + + "&cert=src/test/resources/data/certs/selfSignedCertificate.pem" + + "&cert=src/test/resources/data/certs/issuingCa.pem&cert=certs/BRZStammCA201.pem"); + + keyStoreConfig.validate(); + + final Pair keyStore = keyStoreFactory.buildNewKeyStore(keyStoreConfig); + Assert.assertNotNull("KeyStore is null", keyStore); + Assert.assertNotNull("KeyStore is null", keyStore.getFirst()); + Assert.assertNull("KeyStore is null", keyStore.getSecond()); + + } + + @Test + @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD) + public void inlineKeyStoreEccSuccess() throws EaafException { + final EaafKeyStoreFactory keyStoreFactory = context.getBean(EaafKeyStoreFactory.class); + Assert.assertFalse("HSM Facade state wrong", keyStoreFactory.isHsmFacadeInitialized()); + + final KeyStoreConfiguration keyStoreConfig = new KeyStoreConfiguration(); + keyStoreConfig.setKeyStoreType(KeyStoreType.INLINE); + keyStoreConfig.setSoftKeyStoreFilePath( + "pkcs12:keystore?private=src/test/resources/data/certs/privateEcKey.pem" + + "&cert=src/test/resources/data/certs/selfSignedEcCertificate.pem" + + "&cert=src/test/resources/data/certs/issuingCa.pem&cert=certs/BRZStammCA201.pem"); + + keyStoreConfig.validate(); + + final Pair keyStore = keyStoreFactory.buildNewKeyStore(keyStoreConfig); + Assert.assertNotNull("KeyStore is null", keyStore); + Assert.assertNotNull("KeyStore is null", keyStore.getFirst()); + Assert.assertNull("KeyStore is null", keyStore.getSecond()); + + } + + @Test + @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD) + public void inlineKeyStoreWrongKeys() throws EaafException { + final EaafKeyStoreFactory keyStoreFactory = context.getBean(EaafKeyStoreFactory.class); + Assert.assertFalse("HSM Facade state wrong", keyStoreFactory.isHsmFacadeInitialized()); + + final KeyStoreConfiguration keyStoreConfig = new KeyStoreConfiguration(); + keyStoreConfig.setKeyStoreType(KeyStoreType.INLINE); + keyStoreConfig.setSoftKeyStoreFilePath( + "pkcs12:keystore?private=src/test/resources/data/certs/privateNotExist.pem" + + "&cert=src/test/resources/data/certs/selfSignedCertificate.pem" + + "&cert=src/test/resources/data/certs/issuingCa.pem&cert=certs/BRZStammCA201.pem"); + + keyStoreConfig.validate(); + + EaafConfigurationException error = assertThrows("wrong exception", EaafConfigurationException.class, + () -> keyStoreFactory.buildNewKeyStore(keyStoreConfig)); + assertEquals("internal.keystore.15", error.getErrorId(), "wrong errorcode"); + + } + @Test @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD) public void symmetricSoftwareKeyWithOutConfig() { diff --git a/eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/InlineKeyStoreTest.java b/eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/InlineKeyStoreTest.java new file mode 100644 index 00000000..d4419956 --- /dev/null +++ b/eaaf_core_utils/src/test/java/at/gv/egiz/eaaf/core/test/credentials/InlineKeyStoreTest.java @@ -0,0 +1,140 @@ +package at.gv.egiz.eaaf.core.test.credentials; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.net.URL; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ResourceLoader; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import at.gv.egiz.eaaf.core.exceptions.EaafException; +import at.gv.egiz.eaaf.core.impl.credential.inline.InlineKeyStoreParser; +import at.gv.egiz.eaaf.core.test.dummy.DummyAuthConfigMap; +import lombok.SneakyThrows; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("/spring/test_eaaf_pvp_lazy.beans.xml") +@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD) +public class InlineKeyStoreTest { + + @Autowired + private DummyAuthConfigMap mapConfig; + + @Autowired + private ResourceLoader resourceLoader; + + @Test + @SneakyThrows + public void inlineKeyStoreEccSuccess() throws EaafException { + assertNotNull("no keystore", InlineKeyStoreParser.buildKeyStore( + new URL(null, + "pkcs12:keystore?private=src/test/resources/data/certs/privateEcKey.pem" + + "&cert=src/test/resources/data/certs/selfSignedEcCertificate.pem", + new InlineKeyStoreParser()), + resourceLoader, + mapConfig.getConfigurationRootDirectory())); + + } + + @Test + @SneakyThrows + public void inlineTrustStoreSuccess() throws EaafException { + assertNotNull("no keystore", InlineKeyStoreParser.buildKeyStore( + new URL(null, + "pkcs12:truststore?" + + "cert=src/test/resources/data/certs/selfSignedEcCertificate.pem", + new InlineKeyStoreParser()), + resourceLoader, + mapConfig.getConfigurationRootDirectory())); + + } + + @Test + @SneakyThrows + public void inlineKeyStoreSymSuccess() throws EaafException { + assertNotNull("no keystore", InlineKeyStoreParser.buildKeyStore( + new URL(null, + "pkcs12:keystore?" + + "inlineSecret=mxuqEAXci2cMNU5FCdbxIaNzJoMv%2FWds7j9gY992TTw%3D", + new InlineKeyStoreParser()), + resourceLoader, + mapConfig.getConfigurationRootDirectory())); + + } + + @Test + @SneakyThrows + public void invalidCertFile() throws EaafException { + check("pkcs12:keystore?" + + "private=src/test/resources/data/certs/privateEcKey.pem" + + "&cert=src/test/resources/data/certs/invalidCertificate.pem"); + + } + + @Test + @SneakyThrows + public void missingKey() throws EaafException { + check("pkcs12:keystore?" + + "cert=src/test/resources/data/certs/selfSignedEcCertificate.pem" + + "&cert=src/test/resources/data/certs/BRZStammCA201.pem"); + + } + + @Test + @SneakyThrows + public void missingCert() throws EaafException { + check("pkcs12:keystore?" + + "private=src/test/resources/data/certs/privateEcKey.pem"); + + } + + @Test + @SneakyThrows + public void invalidType() throws EaafException { + check("pkcs12:unknown?" + + "private=src/test/resources/data/certs/privateEcKey.pem"); + + } + + @Test + @SneakyThrows + public void twoKeyFiles() throws EaafException { + check("pkcs12:keystore?" + + "cert=src/test/resources/data/certs/selfSignedEcCertificate.pem" + + "&private=src/test/resources/data/certs/privateEcKey.pem" + + "&private=src/test/resources/data/certs/privateEcKey.pem"); + + } + + @Test + @SneakyThrows + public void twoSymKeyFiles() throws EaafException { + check("pkcs12:keystore?" + + "inlineSecret=mxuqEAXci2cMNU5FCdbxIaNzJoMv%2FWds7j9gY992TTw%3D" + + "&inlineSecret=mxuqEAXci2cMNU5FCdbxIaNzJoMv%2FWds7j9gY992TTw%3D"); + + } + + @Test + @SneakyThrows + public void missingParams() throws EaafException { + check("pkcs12:keystore"); + + } + + private void check(String url) { + assertThrows(IllegalArgumentException.class, + () -> InlineKeyStoreParser.buildKeyStore( + new URL(null, url, new InlineKeyStoreParser()), resourceLoader, + mapConfig.getConfigurationRootDirectory())); + + } + +} diff --git a/eaaf_core_utils/src/test/resources/data/certs/BRZStammCA201.pem b/eaaf_core_utils/src/test/resources/data/certs/BRZStammCA201.pem new file mode 100644 index 00000000..2ff92f54 --- /dev/null +++ b/eaaf_core_utils/src/test/resources/data/certs/BRZStammCA201.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgIIJXWL7AxEWoAwDQYJKoZIhvcNAQELBQAwXDELMAkGA1UE +BhMCQVQxITAfBgNVBAoTGEJ1bmRlc3JlY2hlbnplbnRydW0gR21iSDEPMA0GA1UE +CxMGQlJaIENBMRkwFwYDVQQDExBCUlotU3RhbW1DQS0yLTAxMB4XDTEzMDcwOTA3 +MDU1OVoXDTI4MDcwOTA3MDU1OVowXDELMAkGA1UEBhMCQVQxITAfBgNVBAoTGEJ1 +bmRlc3JlY2hlbnplbnRydW0gR21iSDEPMA0GA1UECxMGQlJaIENBMRkwFwYDVQQD +ExBCUlotU3RhbW1DQS0yLTAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAx5iCz1dd/UHXQcI2l87ZGIXynvZv32NNKJfk2sw73Y51Zxn5WAoU92WLN7jH +ajWVZYN4MqP/gOTpYTioFHVFIURrE3IjNQnaZt53lYBQW7Z7ZlDJG32pGNNeNEWG +0rzrwRwpjdfVNf0eblw64kzfOVgLSvZjoSzpbkIHaB4WWTAdnvJTHZZl2zommKIH +vTaf0nNv1lBWOCDO/uii+nrtFebnOcOmp09zwFoo78N8ujPmjpcO54EoKeczHr0P +PEfEc+tFiPbIMbTG6UNudTXQHMsr/wumCn/ofuD2de2YhrIcl8n7yyIlmekTIQaj +3w+P26WcLySYEzBXA+oZeDeoOzPDWEC18Jd61fe2VhTLTjZ2fi4ykpfjSgsrYjS4 +SFx/3z0Yj+Wv/XVZ7pnWzV4nrGfgL2cooz9ynTMNupt51qO9+zK7ScMeNzN5yPs9 +50N0XmKwFL1vJEIbz0df7ZTLACu5nRtg8yFf7yJJqG7JRRYoQbLZJWM7xHQU/5Mh +cXLH4d5PHj8rlF5maubS3zqBEf+WtIUbt/NV952ETbk6qMLHpWXMU6cJ/xr8fAQE +kL0rukJndtt/goSayN2OsXc3x7ZNN04C2u6J2M1mxe1Eykf+9xXB7dmgUHETnTEX +tvGYsoBRgl/xKQ0PzwSUq+KnzDb990LFmoKIugmydCyomicCAwE4F6N9MHswHQYD +VR0OBBYEFD2c5EOPAym+C0b8jPvP49gkuAWMMA8GA1UdEwEB/wQFMAMBAf8wHwYD +VR0jBBgwFoAUPZzkQ48DKb4LRvyM+8/j2CS4BYwwGAYDVR0gBBEwDzANBgsrBgEE +AbM4j10BATAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHVBo9N4 +G3oeYWfipKDMkEjIDd/XD0o4nl2DM1yb/ROzHvZAligFYl4sbcpjVLB0iB1ej27H +lhXSRI41uUBVIV967c4Wp1RLmeA1n1O2dsdPByGe6B2pDQVo7hWIRaHmCRX3fOBN +JkUuMMj5oGlP9OP2Vgl1c1bk2/CQlc3lwCEGLSkQQfgM6NEtIe+cwFZP91mFqDR9 +EQ1uTjBYmUq377bhqaPgQPDrzsHtOl0qxeOWrKn1mXdp9yiodlmEZ0WVkid90G6x +MjYBZvC5Gl609IF/Rlip71y8kS7d36/+6e/AYudYP+oxggdCd4UF6dizor2HNePn +O/R33dwSqdWroOTmFoCFFHRP11L3GOYqHdDEye03lBgzNKfEz+aKbnk+ic8fBvOm +lY3sqro6q0GGIeVe7UVveO4X9MyAbk7qr1fhnuGSGe2PGh9/bWtpCcSy4YpWIGtC +rbDchVB0HwuCuWbjWyvKcYSAdyomcUXtFv7PLejsS+qSMwHOYD2u08ob0a9jUVSz +MGt1Za0cAuQbdyd2DS+tz/ZPJWUfhskArP2R3TvG0o2VYXaJggFSFBS1lCcbG1c0 +AxDwnb51qVrFnKNHtf41KkNkBurCW1ivp9gLD88KsDXdKMFp833d2CobiOQ7Qdyz +hJ6g+4+HvoPh3ePls8vdtod51vXWXZFf3Rum +-----END CERTIFICATE----- \ No newline at end of file diff --git a/eaaf_core_utils/src/test/resources/data/certs/invalidCertificate.pem b/eaaf_core_utils/src/test/resources/data/certs/invalidCertificate.pem new file mode 100644 index 00000000..bdabde6c --- /dev/null +++ b/eaaf_core_utils/src/test/resources/data/certs/invalidCertificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCjCCAfKgAwIBAgIJAL1NdwD4ilEZMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNV +BAMMD1Rlc3RDZXJ0aWZpY2F0ZTAeFw0yMjAzMDExNDU0NTBaFw0yMzAzMDExNDU0 +NTBaMBoxGDAWBgNVBAMMD1Rlc3RDZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMfBJLAB0zebXutx4vHXRpwbw31SlABtOUvlcZLN0EDV +TKO5NlIgSzlbjI1o+/KcEwIvT3J8MbqBTe7SCxiXKtiDCRPOWYLgGnCiEeyiRu8c +hFFfvXER+Xh2o2H771tdIAglaNWrAREZNmr2iMNITXsvV1RJeEAnax8XviWAAdFg +S7C+jDrRoN7YJ/74tKi+/UJJmjxQDtjds9oErcPvd7aj6RPc8p9SDx3WSL8A0/6y +1AWavNYIbdQOY0xF4tGEnSm6ZLHlS3CZ2dvHlDdEhBEU4z0oSLjWn4Ty19oK46p7 +7YxHEGFKrQnXxICUMbG8KG1ycG4jE5XKT6vxkLdtYSMCAwEAAaNTMFEwHQYDVR0O +BBYEFGJMN/2sdcWk2lhSfCHQZ9yX7BfnMB8GA1UdIwQYMBaAFGJMN/2sdcWk2lhS +fCHQZ9yX7BfnMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACzj +me1bqCbZXjPRIu7UCdIEqpeObd+c0Jd/Ba+tfeXymF1SKdxDo1MaVwHi+BpNfmYM +9hCOTZcLlWY/ejD4XhhV2kYIo+7MxRrGP3B20+hJG+JiGmyDtsmEAWMJG+VZ5G+u +fodyjuPl7KZXujdGwB2di9YIX//VRxyBka7ilq2vaabbcyapFVdB6s1mNEiFaLkrU0 +h7miSPA2p5KfStw4zJ62iyqhiouDUIpMOvblRTkgE4JToCjJn4cfo9gBmwtU1tg5 +VBlO992YteGRKN5FR1C0BkK/R5pnuhcifX7tPA+gGf/G2Jwv/m32huPylVojzVEz +mH3QhlOXXl/1GbJ2vYc= +-----END CERTIFICATE----- diff --git a/eaaf_core_utils/src/test/resources/data/certs/privateEcKey.pem b/eaaf_core_utils/src/test/resources/data/certs/privateEcKey.pem new file mode 100644 index 00000000..d26bf7d6 --- /dev/null +++ b/eaaf_core_utils/src/test/resources/data/certs/privateEcKey.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP3hE9RxmkYpQ+ic7FFJo26fHqMVzY/0idtQ4boi2z9BoAoGCCqGSM49 +AwEHoUQDQgAEN3saobVAMfR09+ngfs0y2QIaDUELTgvtPSgihR4Pr6z9e4HMaeGD +vZXn7knl/dEk8KuhqTmg5Y3d41suKvN9yw== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/eaaf_core_utils/src/test/resources/data/certs/privateKey.pem b/eaaf_core_utils/src/test/resources/data/certs/privateKey.pem new file mode 100644 index 00000000..acceb747 --- /dev/null +++ b/eaaf_core_utils/src/test/resources/data/certs/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHwSSwAdM3m17r +ceLx10acG8N9UpQAbTlL5XGSzdBA1UyjuTZSIEs5W4yNaPvynBMCL09yfDG6gU3u +0gsYlyrYgwkTzlmC4BpwohHsokbvHIRRX71xEfl4dqNh++9bXSAIJWjVqwERGTZq +9ojDSE17L1dUSXhAJ2sfF74lgAHRYEuwvow60aDe2Cf++LSovv1CSZo8UA7Y3bPa +BK3D73e2o+kT3PKfUg8d1ki/ANP+stQFmrzWCG3UDmNMReLRhJ0pumSx5Utwmdnb +x5Q3RIQRFOM9KEi41p+E8tfaCuOqe+2MRxBhSq0J18SAlDGxvChtcnBuIxOVyk+r +8ZC3bWEjAgMBAAECggEAPg9Cn7AheBA8YDfEw1AXrt5tUN08ABnvCCZjr7FjtxDN +u7wKJV/FHy/TLgfk5s4YlcAvFamCJaiQltcI/X3RoOytAEtGwgNwE3g2y2brVNRu +Q33UNHHaKNPrFMEZ7y+yiPVeaau0LCTCJF8txEYNVG0z24rr4jqc7E8LE0l+xxZw +M1WVExtoXfqIfvfCCn/6ZCY0sDJyjMB+Xl5NIUU3nMbK3vzJh1n4vZMq3bSTLp/G +ZKfxkKxpZwpZafLaSy4sjsUwdK79mwEvKYI7OBrUZ6LUOXUlMpiaFl6qUG5iVia+ +T6HW/vQFxQTAKTAXRsFvUGO4oAwPTkWal3VN+9xcUQKBgQDqWEAPpgXmjW2JqRzr +8fcREN1+N8IZNqpaPpP5RZ//NvOz+yTibc/rEwjlcuSERS55YrkldEFsCROT9d7f +CRkwNpF5Ciahy3ngUp9BNyb5gMXnNCPpgg1x/HRC62wJFvZzxE8cb2WhVq2eig/j +GlldhAWtVOyfT/cas+fmW69NyQKBgQDaNpzIVnpJQZnP0Q8IHd4Alb9u4bm3iGM2 +0bwf4qBhOePjGQzZdgzlWJfLxLfnh31Gc3n/x+Vh7vEZCXoipjpEqp3SbDfjcYPg +zvVLKcgp7xTPjFNPVmWkFbsP6HA5KBif/GV00kK7JM5TM5sr6rLYKoCnCdCOxcg7 +eyAw2at9iwKBgGxWpDcXz/6IP622qxJBaLFRFLT3Xhp99T+HUq2ZDKDWbNA2ORUd +I1RQFnrNJOwpd5TSmTnBh0VE5PIwZvrBoA10DMjicn22LgAQ8mMZ9kC+0b9TkY6w +ezrJNY6CfA2vufxHMzO4JEn0You61CFv2wSqtl0tt77nHxwPNBSBKohRAoGAV1ba +g9kREvcd0C8V/CKwM728JG5Wfh4pz8w8pup3VNLt4nypRvTYDofaIeX30cTKIIOh +xf3FUuBnQaUobGvqEIfVqV5FNFS/+x/6z/3GLvqRcszwn2WuQvGrsJ9RI59n08ka +9poduXfu2SBjsD09HgZM/g6QmkSRczVB2iuVVycCgYEAiPUQtNmqPkHrIIbw69CK +Pg3s3/L+Psl4yapuaREFcj1hgemOjsDPtwWCoFPbMnsn5MsiUyyhk5V9aeQL081a +5r87dQxB2jSUAIurNj5nBz4oNqk1MhJMB1Eiv70+LT8rihHM33QYb2bgrwLjF3un ++cCXwCw5u64YJBBMeFRKW8Q= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/eaaf_core_utils/src/test/resources/data/certs/selfSignedCertificate.pem b/eaaf_core_utils/src/test/resources/data/certs/selfSignedCertificate.pem new file mode 100644 index 00000000..a0e8b26c --- /dev/null +++ b/eaaf_core_utils/src/test/resources/data/certs/selfSignedCertificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCjCCAfKgAwIBAgIJAL1NdwD4ilEZMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNV +BAMMD1Rlc3RDZXJ0aWZpY2F0ZTAeFw0yMjAzMDExNDU0NTBaFw0yMzAzMDExNDU0 +NTBaMBoxGDAWBgNVBAMMD1Rlc3RDZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMfBJLAB0zebXutx4vHXRpwbw31SlABtOUvlcZLN0EDV +TKO5NlIgSzlbjI1o+/KcEwIvT3J8MbqBTe7SCxiXKtiDCRPOWYLgGnCiEeyiRu8c +hFFfvXER+Xh2o2H771tdIAglaNWrAREZNmr2iMNITXsvV1RJeEAnax8XviWAAdFg +S7C+jDrRoN7YJ/74tKi+/UJJmjxQDtjds9oErcPvd7aj6RPc8p9SDx3WSL8A0/6y +1AWavNYIbdQOY0xF4tGEnSm6ZLHlS3CZ2dvHlDdEhBEU4z0oSLjWn4Ty19oK46p7 +7YxHEGFKrQnXxICUMbG8KG1ycG4jE5XKT6vxkLdtYSMCAwEAAaNTMFEwHQYDVR0O +BBYEFGJMN/2sdcWk2lhSfCHQZ9yX7BfnMB8GA1UdIwQYMBaAFGJMN/2sdcWk2lhS +fCHQZ9yX7BfnMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACzj +me1bqCbZXjPRIu7UCdIEqpeObd+c0Jd/Ba+tfeXymF1SKdxDo1MaVwHi+BpNfmYM +9hCOTZcLlWY/ejD4XhhV2kYIo+7MxRrGP3B20+hJG+JiGmyDtsmEAWMJG+VZ5G+u +fodyjuPl7KZXujdGwB2di9YIX//VRxyBka7ilq2vxJayapFVdB6s1mNEiFaLkrU0 +h7miSPA2p5KfStw4zJ62iyqhiouDUIpMOvblRTkgE4JToCjJn4cfo9gBmwtU1tg5 +VBlO992YteGRKN5FR1C0BkK/R5pnuhcifX7tPA+gGf/G2Jwv/m32huPylVojzVEz +mH3QhlOXXl/1GbJ2vYc= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/eaaf_core_utils/src/test/resources/data/certs/selfSignedEcCertificate.pem b/eaaf_core_utils/src/test/resources/data/certs/selfSignedEcCertificate.pem new file mode 100644 index 00000000..0661abe7 --- /dev/null +++ b/eaaf_core_utils/src/test/resources/data/certs/selfSignedEcCertificate.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBgzCCASigAwIBAgIJALymKU17CVm/MAoGCCqGSM49BAMCMBwxGjAYBgNVBAMM +EUVjVGVzdENlcnRpZmljYXRlMB4XDTIyMDMxNjE1NTE0NFoXDTIzMDMxNjE1NTE0 +NFowHDEaMBgGA1UEAwwRRWNUZXN0Q2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAAQ3exqhtUAx9HT36eB+zTLZAhoNQQtOC+09KCKFHg+vrP17gcxp +4YO9lefuSeX90STwq6GpOaDljd3jWy4q833Lo1MwUTAdBgNVHQ4EFgQUFZbLWq9o +08GPaT1p8QsWhSO3+ZEwHwYDVR0jBBgwFoAUFZbLWq9o08GPaT1p8QsWhSO3+ZEw +DwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNJADBGAiEApUpgRRjAO0F3Ti2L +bZFC+MltwSZyD3Xg4volOvOlk8QCIQDU3B5qpTQ6+pAxu6PpPFog0VZCMR7Ol7cJ +mjsOOak7TQ== +-----END CERTIFICATE----- \ No newline at end of file -- cgit v1.2.3