[java-idp-integration-tests] branch main updated: Initial test for the OIDC RP plugin

Tom Zeller tzeller at dragonacea.biz
Fri Sep 6 00:21:37 UTC 2024


This is an automated email from the git hooks/post-receive script.

tzeller pushed a commit to branch main
in repository java-idp-integration-tests.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-integration-tests.git;a=commit;h=6c497e3cc50b926a2c423bd612d457e816d3a96f

The following commit(s) were added to refs/heads/main by this push:
     new 6c497e3  Initial test for the OIDC RP plugin
6c497e3 is described below

commit 6c497e3cc50b926a2c423bd612d457e816d3a96f
Author: Tom Zeller <tzeller at dragonacea.biz>
AuthorDate: Thu Sep 5 19:21:27 2024 -0500

    Initial test for the OIDC RP plugin
---
 .../idp/integration/tests/oidc/RPTest.java         | 989 +++++++++++++++++++++
 .../oidc/shibboleth-idp/metadata/oidc-client.json  |  13 +
 .../truststore.asc                                 | 287 ++++++
 3 files changed, 1289 insertions(+)

diff --git a/src/test/java/net/shibboleth/idp/integration/tests/oidc/RPTest.java b/src/test/java/net/shibboleth/idp/integration/tests/oidc/RPTest.java
new file mode 100644
index 0000000..830959e
--- /dev/null
+++ b/src/test/java/net/shibboleth/idp/integration/tests/oidc/RPTest.java
@@ -0,0 +1,989 @@
+
+package net.shibboleth.idp.integration.tests.oidc;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.ProcessBuilder.Redirect;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.regex.Matcher;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.cryptacular.util.CertUtil;
+import org.cryptacular.util.KeyPairUtil;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.ui.ExpectedCondition;
+import org.openqa.selenium.support.ui.WebDriverWait;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.XMLObjectBuilder;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.io.Unmarshaller;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.schema.XSAny;
+import org.opensaml.saml.saml1.core.AttributeValue;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.x509.BasicX509Credential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.util.FileSystemUtils;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import net.shibboleth.idp.integration.tests.BaseIntegrationTest;
+import net.shibboleth.idp.integration.tests.StatusTest;
+import net.shibboleth.idp.integration.tests.util.server.AbstractServerProcess;
+import net.shibboleth.idp.integration.tests.util.server.JettyServerProcess;
+import net.shibboleth.idp.integration.tests.util.server.TomcatServerProcess;
+import net.shibboleth.idp.test.flows.saml2.SAML2TestResponseValidator;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.net.IPRange;
+import net.shibboleth.shared.net.URLBuilder;
+import net.shibboleth.shared.xml.XMLParserException;
+
+/**
+ * Test the OIDC RP plugin for the IdP.
+ */
+public class RPTest extends BaseOPTest {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(RPTest.class);
+
+    /** Path to idp.home for the OP, ends with "shibboleth-op". */
+    protected Path pathToOP;
+
+    /** Server process for the OP. */
+    @NonnullAfterInit
+    protected AbstractServerProcess opServer;
+
+    /** Non-secure port that the OP web server listens on. Defaults to 8081. */
+    @Nonnull
+    protected Integer opPort = 8081;
+
+    /** Secure port that the OP web server listens on. Defaults to 8444. */
+    @Nonnull
+    protected Integer opSecurePort = 8444;
+
+    /** Port to shutdown OP Servlet Container. Defaults to 8006. */
+    @Nonnull
+    protected Integer opShutdownPort = 8006;
+
+    /** Path to jetty.base for the OP, ends with "jetty-base-op". */
+    @Nullable
+    protected Path pathToOpJettyBase;
+
+    /** Path to tomcat.base for the OP, ends with "tomcat-base-op". */
+    @Nullable
+    protected Path pathToOpTomcatBase;
+
+    /** Hostname for the OP. Defaults to "op.tests.shibboleth.net". */
+    @Nonnull
+    protected String opHostname = "op.tests.shibboleth.net";
+
+    /** Non-secure OP base URL. Defaults to http://op.tests.shibboleth.net:8081 */
+    @NonnullAfterInit
+    protected String opBaseURL;
+
+    /** Secure OP base URL. Defaults to https://op.tests.shibboleth.net:8444. */
+    @NonnullAfterInit
+    protected String opSecureBaseURL;
+
+    /** Non-secure private OP base URL. Defaults to http://localhost:8080. */
+    @NonnullAfterInit
+    protected String opPrivateBaseURL;
+
+    /** Secure private OP base URL. Defaults to https://localhost:8443. */
+    @NonnullAfterInit
+    protected String opPrivateSecureBaseURL;
+
+    /** Commands used to start the OP server process. */
+    @NonnullAfterInit
+    protected List<String> opServerCommands = new ArrayList<>();
+
+    /** SP private key resource location. */
+    @Nonnull
+    protected final String SP_KEY = "/credentials/sp.key";
+
+    /** SP certificate resource location. */
+    @Nonnull
+    protected final String SP_CRT = "/credentials/sp.crt";
+
+    /** Response validator. */
+    @Nonnull
+    protected SAML2TestResponseValidator validator;
+
+    @BeforeClass
+    protected void setUpURLs() throws Exception {
+
+        startFlowURLPath = "/sp/SAML2/InitSSO/Redirect";
+
+        loginPageURLPath = "/idp/profile/oidc/authorize";
+
+        responsePageURLPath = "/sp/SAML2/POST/ACS";
+    }
+
+    /**
+     * Copy script to create JWTs from IdP/RP to OP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void copyJwtGenToOP() throws IOException {
+
+        final Path pathToOPBin = pathToOP.resolve("bin");
+
+        Assert.assertTrue(pathToOPBin.toFile().exists());
+
+        final Path pathToOPJWTGen = pathToOPBin.resolve(isWindows() ? "jwtgen.bat" : "jwtgen.sh");
+
+        assert pathToOPJWTGen.toFile().exists() : "Path to jwtgen not found";
+
+        final Path pathToJWTGen = pathToBin.resolve(isWindows() ? "jwtgen.bat" : "jwtgen.sh");
+
+        Files.copy(pathToOPJWTGen, pathToJWTGen, StandardCopyOption.REPLACE_EXISTING);
+
+        final Path pathToDirToCopy = pathToOPBin.resolve("net.shibboleth.idp.oidc.op");
+
+        final Path pathToTarget = pathToBin.resolve("net.shibboleth.idp.oidc.op");
+
+        Files.createDirectory(pathToTarget);
+
+        FileSystemUtils.copyRecursively(pathToDirToCopy, pathToTarget);
+    }
+
+    /**
+     * Get the SP credential.
+     * 
+     * @return the SP credential.
+     * @throws IOException
+     *             if an error occurs reading the credential
+     */
+    @Nonnull
+    protected Credential getSPCredential() throws IOException {
+
+        final ClassPathResource spKeyResource = new ClassPathResource(SP_KEY);
+        Assert.assertTrue(spKeyResource.exists());
+        final PrivateKey spPrivateKey = KeyPairUtil.readPrivateKey(spKeyResource.getInputStream());
+
+        final ClassPathResource spCrtResource = new ClassPathResource(SP_CRT);
+        Assert.assertTrue(spCrtResource.exists());
+        final X509Certificate spEntityCert = (X509Certificate) CertUtil.readCertificate(spCrtResource.getInputStream());
+
+        return new BasicX509Credential(spEntityCert, spPrivateKey);
+    }
+
+    /**
+     * Install the OP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void installOP() throws IOException {
+
+        // Run installer from idp distribution directory
+        final ProcessBuilder installerBuilder = new ProcessBuilder();
+        installerBuilder.directory(pathToIdPDist.toFile());
+        installerBuilder.redirectErrorStream();
+        final List<String> installerCommands = new ArrayList<String>();
+
+        if (isWindows()) {
+            final Path pathToInstallBat = pathToIdPDist.resolve(Paths.get("bin", "install.bat"));
+            Assert.assertTrue(pathToInstallBat.toFile().exists());
+            installerCommands.add(pathToInstallBat.toAbsolutePath().toString());
+        } else {
+            final Path pathToInstallSh = pathToIdPDist.resolve(Paths.get("bin", "install.sh"));
+            Assert.assertTrue(pathToInstallSh.toFile().exists());
+            installerCommands.add(pathToInstallSh.toAbsolutePath().toString());
+        }
+
+        // targetDir
+        installerCommands.add("--targetDir");
+        installerCommands.add(pathToIdPHome.toAbsolutePath().toString());
+        // hostName
+        installerCommands.add("--hostName");
+        installerCommands.add(opHostname);
+        // scope
+        installerCommands.add("--scope");
+        installerCommands.add("example.org");
+        // entityID
+        installerCommands.add("--entityID");
+        installerCommands.add("https://op.example.org");
+        // keystorePassword
+        installerCommands.add("--keystorePassword");
+        installerCommands.add("password");
+        // sealerPassword
+        installerCommands.add("--sealerPassword");
+        installerCommands.add("password");
+        // noPrompt
+        installerCommands.add("--noPrompt");
+
+        installerBuilder.command(installerCommands);
+
+        // Write command used to install the IdP to a file for troubleshooting on Linux
+        if (!isWindows()) {
+            final File fileToInstallIdP = pathToPerTestDirectory.resolve("install-op.sh").toFile();
+            log.debug("Writing command to install the OP to '{}'", fileToInstallIdP);
+            final String commandToInstallIdP = "(\nset -x\n" + pathToIdPDist.toAbsolutePath() + "/"
+                    + String.join(" \\\n", installerCommands) + "\n)";
+            Files.write(fileToInstallIdP.toPath(), commandToInstallIdP.getBytes());
+            fileToInstallIdP.setExecutable(true);
+        }
+
+        // Run installer
+        final Process installerProcess = installerBuilder.start();
+        logProcess(installerProcess, "install :");
+    }
+
+    /**
+     * Set up CLI paths after switching idp.home from OP to RP and vice versa.
+     */
+    protected void setUpCLIPaths() {
+        pathToBin = pathToIdPHome.resolve("bin");
+        Assert.assertTrue(pathToBin.toFile().exists());
+
+        pathToBuildCLI = pathToBin.resolve(isWindows() ? "build.bat" : "build.sh");
+        Assert.assertTrue(pathToBuildCLI.toFile().exists());
+        buildCLI = pathToBuildCLI.toAbsolutePath().toString();
+
+        pathToModuleCLI = pathToBin.resolve(isWindows() ? "module.bat" : "module.sh");
+        Assert.assertTrue(pathToModuleCLI.toFile().exists());
+        moduleCLI = pathToModuleCLI.toAbsolutePath().toString();
+
+        pathToPluginCLI = pathToBin.resolve(isWindows() ? "plugin.bat" : "plugin.sh");
+        Assert.assertTrue(pathToPluginCLI.toFile().exists());
+        pluginCLI = pathToPluginCLI.toAbsolutePath().toString();
+
+        pathToReloadServiceCLI = pathToBin.resolve(isWindows() ? "reload-service.bat" : "reload-service.sh");
+        Assert.assertTrue(pathToReloadServiceCLI.toFile().exists());
+        reloadServiceCLI = pathToReloadServiceCLI.toAbsolutePath().toString();
+    }
+
+    /**
+     * Set up OP.
+     * 
+     * @throws Exception
+     *             if an error occurs
+     */
+    protected void setUpOP() throws Exception {
+        // Save idp.home for RP
+        final Path pathToRP = pathToIdPHome;
+
+        // Create path to idp.home for OP
+        pathToOP = pathToIdPHome.getParent().resolve("shibboleth-op");
+
+        // Temporarily change path to idp.home as OP instead of RP
+        pathToIdPHome = pathToOP;
+
+        idpHome = pathToIdPHome.toAbsolutePath().toFile();
+
+        installOP();
+
+        setUpCLIPaths();
+
+        setUpOPAvailablePorts();
+
+        setUpOPBaseURLs();
+
+        setUpOPJettyBase();
+
+        setUpOPTomcatBase();
+
+        final Path pathToIdPJettyBase = pathToJettyBase;
+
+        pathToJettyBase = pathToOpJettyBase;
+
+        final Path pathToIdPTomcatBase = pathToTomcatBase;
+
+        pathToTomcatBase = pathToOpTomcatBase;
+
+        setUpOPJettyPorts();
+
+        setUpOPTomcatPorts();
+
+        setUpJettySNI();
+
+        copyFromIdPDistToIdPHome("credentials");
+
+        setUpOPPlugin();
+
+        setUpOPIssuerIncludingPort();
+
+        setUpOPMetadata();
+
+        updateClientMetadata("8443", securePort.toString());
+
+        final Path pathToOPLDAPProperties = pathToOP.resolve(Paths.get("conf", "ldap.properties"));
+
+        replaceProperty(pathToOPLDAPProperties, "idp.authn.LDAP.ldapURL", "ldap://localhost:" + ldapPort);
+
+        // setUpDebugLogging();
+
+        // Change path to idp.home back to RP
+        pathToIdPHome = pathToRP;
+
+        idpHome = pathToIdPHome.toAbsolutePath().toFile();
+
+        pathToJettyBase = pathToIdPJettyBase;
+
+        pathToTomcatBase = pathToIdPTomcatBase;
+
+        setUpCLIPaths();
+    }
+
+    /**
+     * Set up ports between 20000 and 30000 for the OP web server to listen on.
+     */
+    protected void setUpOPAvailablePorts() {
+
+        if (Boolean.getBoolean("8080")) {
+            return;
+        }
+
+        final SortedSet<Integer> ports = findAvailablePorts(5);
+        final Iterator<Integer> iterator = ports.iterator();
+
+        opPort = iterator.next();
+        log.debug("Selecting port '{}' for non-secure OP endpoints", opPort);
+
+        opSecurePort = iterator.next();
+        log.debug("Selecting port '{}' for secure OP endpoints", opSecurePort);
+
+        opShutdownPort = iterator.next();
+        log.debug("Selecting port '{}' to shutdown OP Servlet container", opShutdownPort);
+    }
+
+    /**
+     * Set up OP endpoint URLs.
+     * 
+     * Non-secure public URL :
+     * 
+     * http://{@link #opHostname}:{@link #opPort}.
+     *
+     * Secure public URL :
+     * 
+     * https://{@link #opHostname}:{@link #opSecurePort}.
+     * 
+     * Non-secure private URL :
+     * 
+     * http://{@link #privateAddress}:{@link #opPort}.
+     * 
+     * Secure private URL :
+     * 
+     * https://{@link #privateSecureAddress}:{@link #opSecurePort}.
+     */
+    protected void setUpOPBaseURLs() {
+
+        // Non secure URL
+        final URLBuilder urlBuilder = new URLBuilder();
+        urlBuilder.setScheme("http");
+        urlBuilder.setHost(opHostname);
+        urlBuilder.setPort(opPort);
+        opBaseURL = urlBuilder.buildURL();
+        log.debug("URL '{}' is the OP base URL which clients should connect to.", opBaseURL);
+
+        // Secure URL
+        final URLBuilder secureUrlBuilder = new URLBuilder();
+        secureUrlBuilder.setScheme("https");
+        secureUrlBuilder.setHost(opHostname);
+        secureUrlBuilder.setPort(opSecurePort);
+        opSecureBaseURL = secureUrlBuilder.buildURL();
+        log.debug("URL '{}' is the secure OP base URL which clients should connect to.", opSecureBaseURL);
+
+        // Private non secure URL
+        final URLBuilder privateUrlBuilder = new URLBuilder();
+        privateUrlBuilder.setScheme("http");
+        privateUrlBuilder.setHost(privateAddress);
+        privateUrlBuilder.setPort(opPort);
+        opPrivateBaseURL = privateUrlBuilder.buildURL();
+        log.debug("URL '{}' is the private OP base URL.", opPrivateBaseURL);
+
+        // Private secure URL
+        final URLBuilder privateSecureUrlBuilder = new URLBuilder();
+        privateSecureUrlBuilder.setScheme("https");
+        privateSecureUrlBuilder.setHost(privateSecureAddress);
+        privateSecureUrlBuilder.setPort(opSecurePort);
+        opPrivateSecureBaseURL = privateSecureUrlBuilder.buildURL();
+        log.debug("URL '{}' is the private secure OP base URL.", opPrivateSecureBaseURL);
+    }
+
+    /**
+     * Set up Jetty base for the OP.
+     * 
+     * Only if 'tomcat' system property is not 'true'.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpOPJettyBase() throws IOException {
+
+        if (Boolean.getBoolean("tomcat")) {
+            log.debug("Not setting up Jetty because system property 'tomcat' is true");
+            return;
+        }
+
+        // Path to the project build directory.
+        final Path buildPath = Paths.get(TEST_DISTRIBUTIONS_DIRECTORY);
+        log.debug("Path to build directory '{}'", buildPath.toAbsolutePath());
+        Assert.assertTrue(buildPath.toAbsolutePath().toFile().exists(), "Path to build directory not found");
+
+        // Path to jetty.base for the OP
+        pathToOpJettyBase = pathToIdPHome.getParent().resolve(Paths.get("jetty-base-op"));
+        log.debug("Path to jetty.base for OP '{}'", pathToOpJettyBase.toAbsolutePath());
+
+        // Copy jetty-base
+        final Path pathToJettyBaseDist = buildPath.resolve("jetty-base");
+        FileSystemUtils.copyRecursively(pathToJettyBaseDist, pathToOpJettyBase);
+        Assert.assertNotNull(pathToOpJettyBase, "Path to jetty.base for OP not found");
+        Assert.assertTrue(pathToOpJettyBase.toAbsolutePath().toFile().exists(), "Path to jetty.base for OP not found");
+
+        // Make tmp directories exist
+        Assert.assertTrue(pathToOpJettyBase.resolve("tmp").toFile().exists(), "Path to jetty.base/tmp/ not found");
+
+        // set idp.home system property
+        opServerCommands.add(0, "-Didp.home=" + pathToOP.toAbsolutePath().toString());
+
+        // set tmp directory system property
+        serverCommands.add("-Djava.io.tmpdir=" + pathToOpJettyBase.resolve("tmp").toAbsolutePath());
+
+        // Remove testbed webapp
+        final Path pathToTestbedXML = pathToOpJettyBase.resolve(Paths.get("webapps", "testbed.xml"));
+        pathToTestbedXML.toFile().delete();
+    }
+
+    /**
+     * Set up Jetty ports for the OP.
+     * 
+     * Only if 'tomcat' system property is not 'true'.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpOPJettyPorts() throws IOException {
+
+        if (Boolean.getBoolean("tomcat")) {
+            log.debug("Not setting up Jetty ports because system property 'tomcat' is true");
+            return;
+        }
+
+        final Path pathToJettyIdPIni = pathToOpJettyBase.resolve(Paths.get("start.d", "idp.ini"));
+
+        replaceProperty(pathToJettyIdPIni, "jetty.http.host", privateSecureAddress);
+        replaceProperty(pathToJettyIdPIni, "jetty.http.port", Integer.toString(opPort));
+        replaceProperty(pathToJettyIdPIni, "jetty.ssl.port", Integer.toString(opSecurePort));
+    }
+
+    /**
+     * Set up OP plugin.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpOPPlugin() throws IOException {
+
+        final String[] plugins = new String[] { //
+                "net.shibboleth.oidc.common", //
+                "net.shibboleth.idp.plugin.oidc.config", //
+                "net.shibboleth.idp.plugin.oidc.op" };
+
+        installPlugins(plugins);
+
+        assertPluginsAreInstalled(plugins);
+
+        buildWAR();
+
+        createOIDCCredentials();
+
+        enableOIDCCredentials();
+
+        enableOIDCAttributes();
+
+        enableOIDCProfiles();
+
+        enableStaticClientRegistration();
+
+        copyStaticClientRegistrations();
+
+        enableOIDCDiscoveryIncludingPort();
+
+        enableOIDCDiscoveryRewrite();
+
+        setUpIdPAttributes();
+
+        setUpOPIssuer();
+    }
+
+    /**
+     * Set up Tomcat base for the OP.
+     * 
+     * Only if 'tomcat' system property is 'true'.
+     * 
+     * @throws Exception
+     *             if an error occurs
+     */
+    protected void setUpOPTomcatBase() throws Exception {
+
+        if (!Boolean.getBoolean("tomcat")) {
+            log.debug("Not setting up Tomcat because system property 'tomcat' is not true");
+            return;
+        }
+
+        // Path to the project build directory.
+        final Path buildPath = Paths.get(TEST_DISTRIBUTIONS_DIRECTORY);
+        log.debug("Path to build directory '{}'", buildPath.toAbsolutePath());
+        Assert.assertTrue(buildPath.toAbsolutePath().toFile().exists(), "Path to build directory not found");
+
+        log.debug("Path to tomcat.home '{}'", pathToTomcatHome.toAbsolutePath());
+        Assert.assertTrue(pathToTomcatHome.toAbsolutePath().toFile().exists(), "Path to tomcat.home not found");
+
+        // Path to tomcat.base for the OP
+        pathToOpTomcatBase = pathToIdPHome.getParent().resolve(Paths.get("tomcat-base-op"));
+        log.debug("Path to OP tomcat.base '{}'", pathToOpTomcatBase.toAbsolutePath());
+
+        // Copy tomcat-base
+        final Path pathToTomcatBaseDist = buildPath.resolve("tomcat-base");
+        FileSystemUtils.copyRecursively(pathToTomcatBaseDist, pathToOpTomcatBase);
+        Assert.assertNotNull(pathToOpTomcatBase, "Path to tomcat.base not found");
+        Assert.assertTrue(pathToOpTomcatBase.toAbsolutePath().toFile().exists(), "Path to tomcat.base not found");
+
+        // Make tmp directories exist
+        Assert.assertTrue(pathToOpTomcatBase.resolve("temp").toFile().exists(), "Path to temp/ not found");
+
+        // Delete webapps/ROOT directory if it exists
+        final Path pathToRootWebapp = pathToOpTomcatBase.resolve(Paths.get("webapps", "ROOT"));
+        log.trace("Path to ROOT webapp '{}'", pathToRootWebapp.toAbsolutePath());
+        if (pathToRootWebapp.toAbsolutePath().toFile().exists()) {
+            log.debug("Deleting ROOT webapp '{}'", pathToRootWebapp.toAbsolutePath());
+            final boolean deleteRootWebapp = FileSystemUtils.deleteRecursively(pathToRootWebapp);
+            log.trace("Deleted ROOT webapp '{}' result '{}'", pathToRootWebapp.toAbsolutePath(), deleteRootWebapp);
+        }
+
+        // Modify setenv.sh and setenv.bat with per-test idp.home directory
+        final Path pathToSetenvSh = pathToOpTomcatBase.resolve(Paths.get("bin", "setenv.sh"));
+        Assert.assertTrue(pathToSetenvSh.toAbsolutePath().toFile().exists(), "Path to setenv.sh not found");
+        final Path pathToSetenvBat = pathToOpTomcatBase.resolve(Paths.get("bin", "setenv.bat"));
+        Assert.assertTrue(pathToSetenvBat.toAbsolutePath().toFile().exists(), "Path to setenv.bat not found");
+        final String oldTextSetenvSh = "-Didp.home=/opt/shibboleth-idp";
+        final String newTextSetenvSh = "-Didp.home=" + Matcher.quoteReplacement(
+                pathToOP.toAbsolutePath().toString().replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)"));
+        replaceFile(pathToSetenvSh, oldTextSetenvSh, newTextSetenvSh);
+        replaceFile(pathToSetenvBat, oldTextSetenvSh, newTextSetenvSh);
+
+        // Remove testbed webapp
+        final Path pathToTestbedXML = pathToOpTomcatBase
+                .resolve(Paths.get("conf", "Catalina", "localhost", "ROOT.xml"));
+        pathToTestbedXML.toFile().delete();
+    }
+
+    /**
+     * Set up Tomcat ports for the OP.
+     * 
+     * Only if 'tomcat' system property is 'true'.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpOPTomcatPorts() throws IOException {
+
+        if (!Boolean.getBoolean("tomcat")) {
+            log.debug("Not setting up Tomcat ports because system property 'tomcat' is not true");
+            return;
+        }
+
+        final Path pathToCatalinaProps = pathToOpTomcatBase.resolve(Paths.get("conf", "catalina.properties"));
+        replaceFile(pathToCatalinaProps, "tomcat.http.host=.*", "tomcat.http.host=" + privateSecureAddress);
+        replaceFile(pathToCatalinaProps, "tomcat.http.port=.*", "tomcat.http.port=" + Integer.toString(opPort));
+        // Our tomcat-base listens for HTTPS on all addresses by default
+        replaceFile(pathToCatalinaProps, "tomcat.https.port=.*", "tomcat.https.port=" + Integer.toString(opSecurePort));
+    }
+
+    /**
+     * Set up OIDC issuer for the OP.
+     * 
+     * {@inheritDoc}
+     */
+    protected void setUpOPIssuerIncludingPort() throws IOException {
+
+        final Path pathToOIDCProperties = pathToIdPHome.resolve(Paths.get("conf", "oidc.properties"));
+
+        replaceFile(pathToOIDCProperties, "https://idp.tests.shibboleth.net", opSecureBaseURL);
+    }
+
+    /**
+     * Set up static/openid-configuration.json using hostname and port for the OP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpOPMetadata() throws IOException {
+
+        final Path pathToOpenIDConfiguration = pathToIdPHome.resolve(Paths.get("static", "openid-configuration.json"));
+
+        replaceFile(pathToOpenIDConfiguration, secureBaseURL, opSecureBaseURL);
+    }
+
+    /**
+     * Set up the RP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpRP() throws IOException {
+
+        final String[] plugins = new String[] { //
+                "net.shibboleth.oidc.common", //
+                "net.shibboleth.idp.plugin.oidc.config", //
+                "net.shibboleth.idp.plugin.authn.oidc.rp" };
+
+        installPlugins(plugins);
+
+        assertPluginsAreInstalled(plugins);
+
+        buildWAR();
+
+        createOIDCCredentials();
+
+        setUpRPOIDCKeys();
+
+        enableOIDCCredentials();
+
+        enableOIDCAttributes();
+
+        setUpRPClientSecretCredentials();
+
+        setUpRPProxyIssuer();
+
+        setUpRPRedirectURLOrigin();
+
+        setUpRPOIDCSSOProfile();
+
+        setUpRPProxyAuthnFlow();
+    }
+
+    /**
+     * Enable OIDC credentials for the RP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpRPClientSecretCredentials() throws IOException {
+
+        final Path pathToCredentialsXML = pathToIdPHome.resolve(Paths.get("conf", "credentials.xml"));
+
+        assert pathToCredentialsXML.toAbsolutePath().toFile().exists() : "Path to credentials.xml not found";
+
+        final String oldText = "</beans>";
+
+        final StringBuilder newText = new StringBuilder();
+
+        newText.append("<import resource=\"authn/oidc-rp-client-secret-credential.xml\" />\n");
+
+        newText.append(oldText + "\n");
+
+        replaceFile(pathToCredentialsXML, oldText, newText.toString());
+    }
+
+    /**
+     * Enable OIDC keys for the RP
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpRPOIDCKeys() throws IOException {
+
+        final Path pathToOIDCProperties = pathToIdPHome.resolve(Paths.get("conf", "authn", "oidc-rp.properties"));
+
+        replaceFile(pathToOIDCProperties, "#idp.signing.oidc.rs.key", "idp.signing.oidc.rs.key");
+        replaceFile(pathToOIDCProperties, "#idp.signing.oidc.es.key", "idp.signing.oidc.es.key");
+        replaceFile(pathToOIDCProperties, "#idp.signing.oidc.rsa.enc.key", "idp.signing.oidc.rsa.enc.key");
+    }
+
+    /**
+     * Enable OIDC.SSO profile to connect to the OP from the RP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpRPOIDCSSOProfile() throws IOException {
+
+        final Path pathToRelyingPartyXML = pathToIdPHome.resolve(Paths.get("conf", "relying-party.xml"));
+
+        final String oldText = "</util:list>";
+
+        final String newText = "<bean id=\"TestProxyOP\" parent=\"RelyingPartyByName\" c:relyingPartyIds=\""
+                + opSecureBaseURL + "\">\n" + //
+                "    <property name=\"profileConfigurations\">\n" + //
+                "        <list>\n" + //
+                "            <bean parent=\"OIDC.SSO\" p:scopes=\"profile,email\"/>\n" + //
+                "        </list>\n" + //
+                "    </property>\n" + //
+                "</bean>\n";
+
+        replaceFile(pathToRelyingPartyXML, oldText, newText + "\n" + oldText);
+    }
+
+    /**
+     * Enable authentication flow to proxy OP for the RP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpRPProxyAuthnFlow() throws IOException {
+
+        final Path pathToAuthnProperties = Paths.get("conf", "authn", "authn.properties");
+
+        final String oldText = "#idp.authn.flows = Password";
+
+        final String newText = "idp.authn.flows = OIDCRelyingParty";
+
+        replaceIdPHomeFile(pathToAuthnProperties, oldText, oldText + "\n" + newText);
+    }
+
+    /**
+     * Set up issuer for the RP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpRPProxyIssuer() throws IOException {
+
+        final Path pathToOIDCProperties = pathToIdPHome.resolve(Paths.get("conf", "authn", "oidc-rp.properties"));
+
+        replaceFile(pathToOIDCProperties, "https://issuer.com", opSecureBaseURL);
+    }
+
+    /**
+     * Set redirect origin for the RP.
+     * 
+     * @throws IOException
+     *             if an error occurs
+     */
+    protected void setUpRPRedirectURLOrigin() throws IOException {
+
+        final Path pathToOIDCProperties = pathToIdPHome.resolve(Paths.get("conf", "authn", "oidc-rp.properties"));
+
+        replaceFile(pathToOIDCProperties, "https://localhost:8443", secureBaseURL);
+    }
+
+    /**
+     * Setup response validator.
+     * 
+     * Set AuthnContextClassRef.
+     * 
+     * Add @example.org twice to some attributes.
+     * 
+     * TODO remove duplicate suffixes
+     * 
+     * @throws IOException
+     *             if an I/O error occurs
+     */
+    @BeforeMethod
+    protected void setUpValidator() throws IOException {
+        validator = new SAML2TestResponseValidator();
+        validator.spCredential = getSPCredential();
+        validator.authnContextClassRef = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified";
+        if (BaseIntegrationTest.isRemote()) {
+            for (final String ipRange : SAUCE_LABS_IP_RANGES) {
+                validator.subjectConfirmationDataAddressRanges.add(IPRange.parseCIDRBlock(ipRange));
+            }
+        }
+
+        final XMLObjectBuilder<XSAny> anyBuilder = XMLObjectProviderRegistrySupport.getBuilderFactory()
+                .<XSAny>ensureBuilder(XSAny.TYPE_NAME);
+        final XSAny uidValue = anyBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME);
+        uidValue.setTextContent("jdoe at example.org");
+        validator.uidAttribute.getAttributeValues().clear();
+        validator.uidAttribute.getAttributeValues().add(uidValue);
+
+        final XSAny mailValue = anyBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME);
+        mailValue.setTextContent("jdoe at example.org@example.org");
+        validator.mailAttribute.getAttributeValues().clear();
+        validator.mailAttribute.getAttributeValues().add(mailValue);
+
+        final XSAny eppnValue = anyBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME);
+        eppnValue.setTextContent("jdoe at example.org@example.org");
+        validator.eppnAttribute.getAttributeValues().clear();
+        validator.eppnAttribute.getAttributeValues().add(eppnValue);
+    }
+
+    /**
+     * Start the OP server. Uses Jetty by default, and Tomcat if the system property
+     * 'tomcat' is true.
+     * 
+     * @throws ComponentInitializationException
+     *             if the server cannot be started
+     */
+    protected void startOPServer() throws ComponentInitializationException {
+        if (Boolean.getBoolean("tomcat")) {
+            startOPTomcatServer();
+        } else {
+            startOPJettyServer();
+        }
+    }
+
+    /**
+     * Start the Jetty server for the OP.
+     * 
+     * @throws ComponentInitializationException
+     *             if the server cannot be initialized
+     */
+    protected void startOPJettyServer() throws ComponentInitializationException {
+        opServer = new JettyServerProcess();
+        opServer.setServletContainerBasePath(pathToOpJettyBase);
+        opServer.setServletContainerHomePath(pathToJettyHome);
+        opServer.setAdditionalCommands(opServerCommands);
+        opServer.setStatusPageURL(opPrivateBaseURL + StatusTest.statusPath);
+        opServer.setShutdownPort(opShutdownPort);
+        opServer.initialize();
+        if (jettyVersion.startsWith("10") || isWindows()) {
+            // Workaround Maven Surefire Plugin freeze if Jetty 10 or Windows writes to
+            // stdout or stderr
+            opServer.getProcessBuilder().redirectOutput(Redirect.INHERIT);
+            opServer.getProcessBuilder().redirectError(Redirect.INHERIT);
+        }
+        opServer.start();
+    }
+
+    /**
+     * Start the Tomcat server for the OP.
+     * 
+     * @throws ComponentInitializationException
+     *             if the server cannot be initialized
+     */
+    protected void startOPTomcatServer() throws ComponentInitializationException {
+        opServer = new TomcatServerProcess();
+        opServer.setServletContainerBasePath(pathToOpTomcatBase);
+        opServer.setServletContainerHomePath(pathToTomcatHome);
+        opServer.setAdditionalCommands(opServerCommands);
+        opServer.setStatusPageURL(opPrivateBaseURL + StatusTest.statusPath);
+        opServer.setShutdownPort(opShutdownPort);
+        opServer.initialize();
+        opServer.start();
+    }
+
+    /**
+     * Stop the server for the OP.
+     */
+    @AfterMethod
+    protected void stopOPServer() {
+        if (opServer != null) {
+            opServer.stop();
+        }
+    }
+
+    /**
+     * Unmarshall the XML response into a SAML 2 {@link Response} object.
+     * 
+     * @param response
+     *            the XML response
+     * @return the SAML 2 Response object
+     * @throws UnsupportedEncodingException
+     *             if an error occurs
+     * @throws XMLParserException
+     *             if an error occurs
+     * @throws UnmarshallingException
+     *             if an error occurs
+     */
+    @Nonnull
+    protected Response unmarshallResponse(@Nullable final String response)
+            throws UnsupportedEncodingException, XMLParserException, UnmarshallingException {
+        final XMLObject xmlObject = unmarshallXMLObject(response);
+        Assert.assertTrue(xmlObject instanceof Response);
+        return (Response) xmlObject;
+    }
+
+    /**
+     * Unmarshall the XML response into an {@link XMLObject} object.
+     * 
+     * @param response
+     *            the response to unmarshall
+     * 
+     * @return the unmarshalled response
+     * 
+     * @throws UnsupportedEncodingException
+     *             if an error occurs
+     * @throws XMLParserException
+     *             if an error occurs
+     * @throws UnmarshallingException
+     *             if an error occurs
+     */
+    @Nonnull
+    protected XMLObject unmarshallXMLObject(@Nullable final String response)
+            throws UnsupportedEncodingException, XMLParserException, UnmarshallingException {
+        Assert.assertNotNull(response);
+        final Document doc = parserPool.parse(new ByteArrayInputStream(response.getBytes("UTF-8")));
+        final Element element = doc.getDocumentElement();
+        Assert.assertNotNull(element);
+        final Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
+        Assert.assertNotNull(unmarshaller);
+        return unmarshaller.unmarshall(element);
+    }
+
+    /**
+     * Validate SAML 2 {@link Response}.
+     * 
+     * @throws Exception
+     *             if an error occurs
+     */
+    protected void validateResponse() throws Exception {
+        validator.validateResponse(unmarshallResponse(getPageSource()));
+    }
+
+    /**
+     * Wait for OP login page.
+     */
+    protected void waitForOPLoginPage() {
+        (new WebDriverWait(driver, Duration.ofSeconds(10))).until(new ExpectedCondition<Boolean>() {
+            public Boolean apply(WebDriver d) {
+                return d.getCurrentUrl().startsWith(opSecureBaseURL + loginPageURLPath);
+            }
+        });
+    }
+
+    @Test(enabled = false)
+    public void testProxyAuthentication() throws Exception {
+
+        setUpOP();
+
+        copyJwtGenToOP();
+
+        startOPServer();
+
+        setUpRP();
+
+        startServer();
+
+        startBrowser();
+
+        startFlow();
+
+        waitForOPLoginPage();
+
+        login();
+
+        waitForResponsePage();
+
+        validateResponse();
+    }
+
+}
diff --git a/src/test/oidc/shibboleth-idp/metadata/oidc-client.json b/src/test/oidc/shibboleth-idp/metadata/oidc-client.json
index e3ff316..724b03c 100644
--- a/src/test/oidc/shibboleth-idp/metadata/oidc-client.json
+++ b/src/test/oidc/shibboleth-idp/metadata/oidc-client.json
@@ -1,4 +1,17 @@
 [
+  {
+    "scope": "openid email profile",
+    "redirect_uris": [
+      "https://idp.tests.shibboleth.net:8443/idp/profile/Authn/OIDC/RP/callback"
+    ],
+    "client_id": "client_id",
+    "client_secret": "client_secret",
+    "response_types": [
+      "code",
+      "id_token",
+      "id_token token"
+    ]
+  },
   {
     "scope": "openid email",
     "redirect_uris": [
diff --git a/src/test/resources/net/shibboleth/idp/integration/tests/truststores/net.shibboleth.idp.plugin.authn.oidc.rp/truststore.asc b/src/test/resources/net/shibboleth/idp/integration/tests/truststores/net.shibboleth.idp.plugin.authn.oidc.rp/truststore.asc
new file mode 100644
index 0000000..5219b48
--- /dev/null
+++ b/src/test/resources/net/shibboleth/idp/integration/tests/truststores/net.shibboleth.idp.plugin.authn.oidc.rp/truststore.asc
@@ -0,0 +1,287 @@
+
+
Philip David Smart <philip.smart at jisc.ac.uk>	id	B8A3DC52
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v at RELEASE_NAME@
+
+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
+AAEAEAAAkgoABQAAAAEAAAKqkhQAAwAAAAQAAAKyknwABwAAA+IAAAK6kpEAAgAA
+AAQ3MTAAkpIAAgAAAAQ3MTAAoAAABwAAAAQwMTAwoAIABAAAAAEAAACWoAMABAAA
+AAEAAADqohcAAwAAAAEAAgAAowEABwAAAAEBAAAApAIAAwAAAAEAAAAApAMAAwAA
+AAEAAAAApAUAAwAAAAEAHAAApAYAAwAAAAEAAAAApDIABQAAAAQAAAacpDMAAgAA
+AAYAAAa8pDQAAgAAACIAAAbCAAAAAAAAAAEAAAARAAAACQAAAAUyMDE4OjA3OjE2
+IDEwOjA3OjM4ADIwMTg6MDc6MTYgMTA6MDc6MzgAAAAKdAAAApMAAAhvAAAE+QAA
+KOIAAA/BAAAAAAAAAAEAAAGPAAAAZAXXBg4DvwPBQXBwbGUgaU9TAAABTU0AEwAB
+AAkAAAABAAAACQACAAcAAAIuAAAA+AADAAcAAABoAAADJgAEAAkAAAABAAAAAQAF
+AAkAAAABAAAA6AAGAAkAAAABAAAA8QAHAAkAAAABAAAAAQAIAAoAAAADAAADjgAM
+AAoAAAACAAADpgANAAkAAAABAAAAKAAOAAkAAAABAAAABAAPAAkAAAABAAAAAgAQ
+AAkAAAABAAAAAQARAAIAAAAlAAADtgAUAAkAAAABAAAABQAXAAkAAAABAAAAAAAZ
+AAkAAAABAAAAAAAaAAIAAAAGAAAD3AAfAAkAAAABAAAAAAAAAABicGxpc3QwME8R
+AgBHAkwCUQJVAlICSwJDAjsCLAIaAnMBzgCQAIgAiAB/AEsCUgJWAlYCTgJGAj4C
+MQIgAsoBjwB/AHkAgACBAIAATgJXAloCVgJOAkQCOAIlAg8CMwFwAHcAjACKAJYA
+jwBMAlYCVwJQAkYCOgIrAhIC4wGiAGUAkgCJAI4AjQCSAEoCUgJQAkcCOgIqAhYC
+9AFqAXgAdACRAIUAhQCMAIsASQJKAvYB+gC8AOIAjgGJAbEAcQBrAIMAeQB6AH4A
+gQBFAoEBTQDQALEA4QDiAL0ArgCZAIkAYwBoAHkAdQBuAEECBAF5ABgBtADHANUA
+uQCkAN0AyQCQAIMAfABqAGUAOwL2AHIAIgEEAb0ArACYAIYAuwDQAK8AXgBmAG8A
+eAAxAgkBUADvAIsAqwC/AJ0AbQB5AF8AZABvAHwAdAB1ACYC5gG3AHUAdgCuANwA
+DAHGAGIAYQBqAHEAdwBvAHAAHQIaAv8BtAGjAZ0BhwF4ATEBcwBtAHYAdQB0AGsA
+ZwAYAhkCDQL5AegB2gHFAagBcgGtAF8AdgBzAHIAagBoABUCGQIUAgkC/AHoAc0B
+sAGPARYBZgBmAHkAagBzAGYAEwITAg8CBAL4AeUBygGzAZsBfgHNAGEAcgBpAGsA
+YQAQAg0CCQL/AfQB4gHJAbMBoAGMAXwBUwEDAeAAcAByAAAIAAAAAAAAAgEAAAAA
+AAAAAQAAAAAAAAAAAAAAAAAAAgxicGxpc3QwMNQBAgMEBQYHCFVmbGFnc1V2YWx1
+ZVl0aW1lc2NhbGVVZXBvY2gQARMAAAU6zneKsRI7msoAEAAIERcdJy0vOD0AAAAA
+AAABAQAAAAAAAAAJAAAAAAAAAAAAAAAAAAAAP///xsEAChy8//+2DAAAShH///v9
+AArWDwAAADsAAAEAAAAAZQAAAQA0MkRDQUE3RC0wMTU0LTRBNTktQjY2NS03QTZB
+QjZBRkM4OEQAAHE4MjVzAAA/1d8AD/+1AD/V3wAP/7UAAAAJAAAABQAAAAkAAAAF
+QXBwbGUAaVBob25lIDcgYmFjayBjYW1lcmEgMy45OW1tIGYvMS44AAANAAEAAgAA
+AAJOAAAAAAIABQAAAAMAAAeGAAMAAgAAAAJXAAAAAAQABQAAAAMAAAeeAAUAAQAA
+AAEAAAAAAAYABQAAAAEAAAe2AAwAAgAAAAJLAAAAAA0ABQAAAAEAAAe+ABAAAgAA
+AAJUAAAAABEABQAAAAEAAAfGABcAAgAAAAJUAAAAABgABQAAAAEAAAfOAB8ABQAA
+AAEAAAfWAAAAAAAAADMAAAABAAAAIAAAAAEAAA5QAAAAZAAAAAMAAAABAAAACwAA
+AAEAABWDAAAAZAAAWb0AAADZAAAArQAABJ4AAN2FAAABWAAA3YUAAAFYAAAACAAA
+AAH/4Q1XaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVn
+aW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBt
+ZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2
+LjAuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5
+OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFi
+b3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIg
+eG1sbnM6bXdnLXJzPSJodHRwOi8vd3d3Lm1ldGFkYXRhd29ya2luZ2dyb3VwLmNv
+bS9zY2hlbWFzL3JlZ2lvbnMvIiB4bWxuczpzdEFyZWE9Imh0dHA6Ly9ucy5hZG9i
+ZS5jb20veG1wL3NUeXBlL0FyZWEjIiB4bWxuczphcHBsZS1maT0iaHR0cDovL25z
+LmFwcGxlLmNvbS9mYWNlaW5mby8xLjAvIiB4bWxuczpzdERpbT0iaHR0cDovL25z
+LmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL0RpbWVuc2lvbnMjIiB4bWxuczpwaG90
+b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcDpD
+cmVhdGVEYXRlPSIyMDE4LTA3LTE2VDEwOjA3OjM4LjcxMCIgeG1wOkNyZWF0b3JU
+b29sPSIxMS40IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOC0wNy0xNlQxMDowNzozOCIg
+cGhvdG9zaG9wOkRhdGVDcmVhdGVkPSIyMDE4LTA3LTE2VDEwOjA3OjM4LjcxMCI+
+IDxtd2ctcnM6UmVnaW9ucyByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+IDxtd2ct
+cnM6UmVnaW9uTGlzdD4gPHJkZjpTZXE+IDxyZGY6bGk+IDxyZGY6RGVzY3JpcHRp
+b24gbXdnLXJzOlR5cGU9IkZhY2UiPiA8bXdnLXJzOkFyZWEgc3RBcmVhOnk9IjAu
+NTEyOTk5OTk5OTk5OTk5OSIgc3RBcmVhOnc9IjAuMjM3OTk5OTk5OTk5OTk5OTki
+IHN0QXJlYTp4PSIwLjM3MSIgc3RBcmVhOmg9IjAuMzE3OTk5OTk5OTk5OTk5OTUi
+IHN0QXJlYTp1bml0PSJub3JtYWxpemVkIi8+IDxtd2ctcnM6RXh0ZW5zaW9ucyBh
+cHBsZS1maTpBbmdsZUluZm9ZYXc9IjAiIGFwcGxlLWZpOkFuZ2xlSW5mb1JvbGw9
+IjI3MCIgYXBwbGUtZmk6Q29uZmlkZW5jZUxldmVsPSIxMDAwIiBhcHBsZS1maTpU
+aW1lc3RhbXA9IjEzNzk5OTkyMTQ1MCIgYXBwbGUtZmk6RmFjZUlEPSI5Ii8+IDwv
+cmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpsaT4gPC9yZGY6U2VxPiA8L213Zy1yczpS
+ZWdpb25MaXN0PiA8bXdnLXJzOkFwcGxpZWRUb0RpbWVuc2lvbnMgc3REaW06aD0i
+MzAyNCIgc3REaW06dz0iNDAzMiIgc3REaW06dW5pdD0icGl4ZWwiLz4gPC9td2ct
+cnM6UmVnaW9ucz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94Onht
+cG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0
+IGVuZD0idyI/PgD/7QB4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAD8cAVoAAxsl
+RxwCAAACAAIcAj8ABjEwMDczOBwCPgAIMjAxODA3MTYcAjcACDIwMTgwNzE2HAI8
+AAYxMDA3MzgAOEJJTQQlAAAAAAAQOGix0QQ3mMOAExf+M5Hq4v/iAkBJQ0NfUFJP
+RklMRQABAQAAAjBBREJFAhAAAG1udHJSR0IgWFlaIAfQAAgACwATADMAO2Fjc3BB
+UFBMAAAAAG5vbmUAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtQURCRQAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmNwcnQA
+AAD8AAAAMmRlc2MAAAEwAAAAa3d0cHQAAAGcAAAAFGJrcHQAAAGwAAAAFHJUUkMA
+AAHEAAAADmdUUkMAAAHUAAAADmJUUkMAAAHkAAAADnJYWVoAAAH0AAAAFGdYWVoA
+AAIIAAAAFGJYWVoAAAIcAAAAFHRleHQAAAAAQ29weXJpZ2h0IDIwMDAgQWRvYmUg
+U3lzdGVtcyBJbmNvcnBvcmF0ZWQAAABkZXNjAAAAAAAAABFBZG9iZSBSR0IgKDE5
+OTgpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAA81EA
+AQAAAAEWzFhZWiAAAAAAAAAAAAAAAAAAAAAAY3VydgAAAAAAAAABAjMAAGN1cnYA
+AAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAWFlaIAAAAAAAAJwYAABPpQAABPxY
+WVogAAAAAAAANI0AAKAsAAAPlVhZWiAAAAAAAAAmMQAAEC8AAL6c/8AAEQgA6gCW
+AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQ
+AAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHw
+JDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp
+anN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TF
+xsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEB
+AQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUh
+MQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4
+OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWW
+l5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp
+6vLz9PX29/j5+v/bAEMAGBgYGBgYKRgYKTopKSk6Tjo6OjpOY05OTk5OY3djY2Nj
+Y2N3d3d3d3d3d4+Pj4+Pj6enp6enu7u7u7u7u7u7u//bAEMBHR8fMCwwUiwsUsSF
+bYXExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE
+xMTExP/dAAQACv/aAAwDAQACEQMRAD8A6KiiisiwpaSkJ4oAZK2xd3uB+fFP3CqL
+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
+=+Re0
+-----END PGP PUBLIC KEY BLOCK-----

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list