[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
+yJLbkSHG8HisFNVlKgHsNx9z7U7AdYWA6mkSRJOUIOK59NWaWHaqbpT949F/z7VU
+sbgR3u18qrZBBOMHt+FFgudLdzGC3eQdR/U4plndfaolcD6//WqvqhB0+TB5wD+R
+qvZ3QWAMdqgDCrnGcUAbmcVnS6gkc/2cAljtxj/aP9KgbVI8HjOOuCD+nWuc84rc
+iYHgMDn2z/hQkI7v2pFO5Q3rVR7qMRiUHgdacJ44lSNjzgY96Qy3RTA2VBNOBBGR
+3oAWiiigAooooA//0OiopKM1kaDZHCLk1z099I26ZT8oO1R+maXV7jcwiQ9B/Osm
+dgifZ+pRv8/zppCbFRv3ZllJI6KM9exqkW5zQWPTtTT61ZID5eRTyc1FnsacB2oE
+S+dKy7C7Y9MnH5U4TybPKzwOKreop49aBjt1OzmmH1pM8UATLM4XGSRxwfbpVn7U
+7yq7HJFZ465pwPNKwHYpdA5T0J/KpLScSbVB6Er+VctDK6knOR3rf0kFsue24/mf
+/rGpaKNyiiikAUUUUAf/0ehqmJ1G8ucYNW6x9RiIUzL34NYmhi6g+Z1kHBIz+dUC
+xJyetSzyGRuar7TWiIF69KNnekwRzTstTAYRSZqTBNBQ0AMJzQDinbDThEx7UBYY
+abVjyW9KTyW9KVwsQZpRTzGR1pmKYE0Z5ye9dZpGDbkjjnH5Vx4OK1dMuxDON5+U
+qQf5j8uaTGdlRTEdXUOpBB6EdKfUDCiiimI//9LeNU7xglu7E8AZq2TgZrldQuHe
+Z4g2VHGMYrJFsy2bJzihVJpwXJwOSa1YLcKMnrVN2ElcopbO9WksfWtFVAqUCocj
+RRKK2SCpfsielXRS0rlcqKi2kY6iphboOcVKM07mlcLFfyUHamGBSelWqaaLhYov
+aKRWbPZsvK1v1G6g1SkJxOTIIODQDg1q3duPvLWSwNaJ3MmrHW6PdRmEWxPzr0Hq
+K3M159BKY5klH8JBr0BTkAmkwQ6iiikM/9PTvLgW0Blxkjp9a493MjtK3Vjmt7WJ
+cRrF/eOfyrnh0rOJTLNsmW3GtpRxWdaKMVpgVMjSI4YpwpBTuaksdSjNIM04fWkM
+MGlwaMn1pOfWgQvNNNLk0mRQMbSGlyKQ0wK8yBl5rAnj2ua6RhkVh3Yy2auJlNFF
+CQcjgiu50+c3FqjnqBg59a4auo0OUmN4ic4OQParZmjfopKKgo//1K+sHM6DttNZ
+ArW1b/Xr/u1kjk1C2KZrWowuavCq0K7EAqcVmzVD8mnAtTBUlIoUE08ZpoFSUhiZ
+PtSZNLSgcUANyaTNPxSGgCPiilNJTAQ8isW8Ta2fWto1QvUDRFv7vNVEiS0MI8Gt
+fRiVvBjGMHP44rJYd60dLP8Apkf41ozE7SiiioKP/9WPWIsFZfwrHiGZFrodYXNs
+rf3WH68Vg2g3TAemTWa2L6mwtOLBetQySCMe9U/OYnJ61Ni7mh5oAyaVbhM4yKzy
+0j9ttHk9807BzGykit0qUc1kRqyHg1pRvxSaKTJsU7GBTAc04nApDIJZ1j6mqhvF
+PXipZVDnmqbxxCqSJbZbWZW6HNO8wd6zeF+4/wCdG6T6j2osHMaoORUTjcpHrVaO
+Q9KtZyuaQXOdbgkelWdPYLdxk9N39Krz8SsPeruloHvVHpk/lWhkdoKKQcDFFQM/
+/9a9frvtJB7VzliMyk+1dDfcwe2awrFSJGz2H9ayWxZYkXcxzUJB3bE61dI71Sfc
+WITigqxII4x985PuaUxREfKPxFUriLYFI5z3qOEFpEVMjOAfr3p2DmLoZozg8irU
+cmelQFSjbJOR605UKHjpSGa0RyKbI2KjibApshzUlFaRz0FQAKT83NWGUDk9aqyR
+M0bt3A4FUInDxDg4pTGpG5P0rHABUrjnOc+3pWgsLxojocNjkU7EqVyyq7utTKMD
+FNiORzU2Kkqxg3sZSYn15p+nytFdK6jJwR+dT6kn3X/Co7GMjMntirvoZ21Ostrg
+XAPGCvWrOKzNNHDmtSpG1qf/1714u+3YfSsm2Qo0hPritqbmJvpWXGeD9ayNUPIz
+ULJg5qegipLsVmAYYYZFIixocooB9asbAaXZTuFiH8KDyamI2iowKAJlHFBGacvS
+jvSKIskcUzOO1TY5p/lg8igViqI06hFz64qTGal2CnBR2p3FYaq4p9OApp4pDsU7
+5N0Deo5FNtVAiVfQCprk/uW9xinRrsRVPYVXQlLU0LBdqt9a0c1UtR+7OPWrVBMt
+z//Q05P9W30NZKHlhWs43KR6isjOGH5GsTVE1LTAafmkWhRS02jNBQjc02lagDNA
+iRTR3p6IO9DAUDEI70vOKapwcGpCOKAQ2lFNGaXNAx1MY0uaYxoJZBMN21PU5/Kp
+B8zZ/hFAXcTnpjFTKuflUfSmJGjajEP1JqxTUXYgT0FOpmbP/9HUNZlxHh9/Y1pG
+oyAeDWRpczgafUfQkU8Ui0OpV5NIKkXikURyEAiq3musmMZX1qw+D1qttweKaJZZ
+E1RyySkfusZ9TQExyakCfLntTC4kbMRhutWx0quuF6VMrA1LGgIppqWojQMKYadT
+DQIegOOAav28LKd78egotP8AUj6mrVUQ5dB1FJRQSf/S0mplPamVkWUJRiQimipb
+gYYN68VEDSZaJBSs2OlNB4zULS7OTRYdxSSacAOpqobjJwoNIJjn5lNOwI0D83Q0
+uVVdpNZ3m85UGnrI/XaaLFWLwIo6dKo7pT2FPDzZwoosJo0FbtQ1VkZs89asMcgU
+mhXGmm0tNwScDvQM1LcYhX86s1Eo2gKO3FPFMyHUtJRQB//T0mqOpDUZrIsilXeh
+HfqKog9q0aoyrsfI6GgaYqHsaYyAtzSr60ueaRRGVU9aQIOxqQgmmmLPTNBSY4Lj
+0p2D6ioPJb1PFOWF/wC8aY+ckCZ6mngBeBTVhYHrUoAX60hOQ0rzmnseKQ0hPGKC
+RCeKmtk3Sbz0X+dVuScVJDfQxO0Eg27OrdqaQmzXpwpikMAw5Bp4oJHUUlLQB//U
+0jURqQ1FWRYVFIocbTUlNNMCiPlO09RS5BNRswkd8fwnFR7iODSsO5aB4p2arBhU
+qsOlKw7k1ANN3cUAgcGgZLnim5pu4Gk3CiwXH0xjxURkycClCnrTsTcmQdzWLc/6
+9z6nH6VuqOM1h3IzJNzjkH9BVRCWxY0y6dbgK7Eq/BB9exrqhXKWMaYfJBJPTuPQ
+j6GunQkqC3Xv9aJEolpaZmlzUjP/1b5php1MNZFiVBOxWMlcZ7ZqY1UuwDGc+lNC
+ZnWZwHU9c1Zcd6o2f33rRbpTe41sVyvpSh2FLTT3oAeJDS+YfSoqUUAP3t0FKAzd
+aFqwoFACKoA4qULk4ph61OnSpGhTwMVh3OfMYrxmtlu9Y1x/rDQhsk09gsyhuQeC
+PWukiJBaNjll4P19axlAFohHtWhbH959VH9a0lsZrcv0tJRWRR//2YkCTgQTAQgA
+OBYhBLW13TMhQq1lfo2HrH0n5hC4o9xSBQJgN4HxAhsDBQsJCAcCBhUKCQgLAgQW
+AgMBAh4BAheAAAoJEH0n5hC4o9xSm/AQAM1VQ6ulVGptsosNJYB+HigxIMZiXtIn
+tddo1oJ2HzucgHGF2VJemu5m2NxKrckNQdlO5AvYiD3cGsx1GCcUCqrVJnpVSzn0
+x2vAUd5Iu5AO6VLuWfz6lLzLYFmR7le1eEVV9PWpiFo5JmhCxFlmzbJ/LXg2rm+b
+gGwYg9PaW4zeJR721BE0YYzAdiiyih0oYQqm1rIpp5+rzHL34UeajNU8krcViWDv
+Q6qSKnkk4RQzzWWo5ByjDxB9UBF5eX1Ls/CyFzCpsNaPooKAxC1MQy53GrQIN4Wq
+Y73BMsfEutMvdWxgL5/zWKIS5Fd+hD9wTtOotu00PzamCq+5CnsggILpJenrHWSR
+6xoSgBL6iYjAq1DaLvGUv8/+gnblLkdh3A9+lCqqb45yAbBlZS4w12Jmfov+WKLS
+e464WTG0VTySkShhrwHKWkekXzCusx8nIow1FKo257N97fMLJFWRlBhCHcgtoYmq
+3a49CUndqAmM9MT1/8RX21xUq/vI3aTSXqnjgo+y5EMKNM6Q8u0KgVvxzNRyoyh6
+cJWgW+lXM55X8M1SDBruQBfTF2lnJbxq02hXyuDhWbtuhLCs01z3AxO42PZVuwqI
+/U7chZGzC2e02DPY0TVwbGJU6SEKg6wrZEeAzH7igRWR2AHGUEX0AzEkZD72mvZZ
+TMtW3ci6/EGZiQIzBBMBCAAdFiEEh0AgjA6T/sgO6/kTt3xS7sIXcd0FAmA3iFkA
+CgkQt3xS7sIXcd26xQ//dvjcgYkVg0KdAUQWE0KWFwHgW5mJWyITZs5PcwkArIGh
+pJCns9CRq5EGwGQuLWvhWBAloH+qFI83ydptPoxztZoy7keK1tCdDTNhk2RLhp98
+4CP5oObh6xgpuMQ1hVtwwdhOErGpU/auHgIr4qtScvsfLlhoXHCttzvnBkgiGKi+
+bn7hH+mGS6XCDDTXggh4s3wRZ/snaqy8WcaprfK3dw5Tpgck0pexikVcyI6sbruu
+euGwbUZ5JwuFNnRQzmjPqJk6ZC3tYqPslZFBKi83HXq4T2liTSI2m8eK/gWU512G
+T4HeLlkiSMLYOJeX+gKI25StkzXBqQoVKQJVc6RG8wdv34hSdFT0h1T6xtGKmb6y
+W0c855yFrp2yefSS9+R3DFxBEqo7YtGF1J53v87GBeYnFMRw6vZnon+Kurttn8ah
+gZP0veyb1JtCgBWAS6HVb2q6k/Uw+nt++NAZbr69rtnyzyrMNVvMnuM/L1diYfD0
+bncHd1Oj8KTHmFj7J9He2ytelfB/MFUmnkPWtAugPtnQMTGPYyb4VsOF53QsR0+8
+bJGGI7M3tk/C03sXGfMpYMUyKK7uyGWbKg97iKXQLIoHaI3zbIYNUMh//JspxCqg
+14GbQx8yaLDdyeORn8wmgsVy9qNEsc/PHJO8644KNZJM9x1fDihztBsRbRzwjVaJ
+AjMEEgEIAB0WIQRebW6uFsPadUULIZyagE6X1wecdwUCYED2egAKCRCagE6X1wec
+d5gKD/42YYeUFgaQDABPG45afoqwyP8H1xDi6W5F47wVFwI1GdSBmIqKGhNONwYS
+toeFKcX1ed0gBbFVkwaWEFKCeo0VjWcLYE1YaxL8ybo+iVrFfd/oEHyf414uvnur
+4jNVwqUmtYQMaLlXq2a9tVAGNtsgqsZKEYBanWDXm24z5l2rDGxLlVi7xTGU3pZE
+LfTF3HvZlN3l/ggagXF+2ocT5O0vTX1OO8x0lknTqhbjTJfi+ObJxV4DyE0ifQyh
+3smBUyxZwCcF9MhRKYnEcf8gbsCPDLoiwCGz0o7cKz2blm7NsTgxZXoS0HRx0/wy
+QHS+6mIDuH1gMLeEvJUql7cdKqGAbG5QD9xGPoW6PiTpFTtVusjxD4dIvil5oth5
+r9btVNtoSysEuL13BjBkrO4JuqVZvEdHFGjNE00q6JrI/SpeBZrRR/YZvLCgUZ9J
+w9S6g/usazLEAnOs2EH37yMFA3h3Jr+8l31CkGWBlbLHWc/6ufuyYo4F98TJ7WRV
+vq2RbAMziHMkDH47iMt2PkycLhqPplzbTyVVS/2r1u1d8vfhWlznASuT0/d6cpi0
+H9L/nqe2Mkst32e/X7jr54lSfURmKC+plNn4QQY61DcaUDw5UsMvl/ieDg7OqBeI
+JVgitbDLfIC15B5oRHalAmBOE9OulvvnqxZCHdQ63XunRIbviw==
+=+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