From f21e806fb7f377ba89f2e4b168fe5945f1ea1668 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Wed, 16 Nov 2016 15:31:51 +0100 Subject: fix bug in eIDAS SAML-engine: generate metadata extensions element from wrong schema definition --- .../id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'id/server') diff --git a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java index 09c3dff38..ab41c2369 100644 --- a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java +++ b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java @@ -35,6 +35,7 @@ import org.joda.time.DurationFieldType; import org.opensaml.Configuration; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.common.Extensions; +import org.opensaml.saml2.common.impl.ExtensionsBuilder; import org.opensaml.saml2.core.Attribute; import org.opensaml.saml2.core.AttributeValue; import org.opensaml.saml2.metadata.AssertionConsumerService; @@ -514,7 +515,14 @@ public class MOAeIDASMetadataGenerator extends MetadataGenerator { } private Extensions generateExtensions() throws EIDASSAMLEngineException { - Extensions eidasExtensions = BuilderFactoryUtil.generateExtension(); + /**FIXME: BuilderFactoryUtil.generateExtension() generates extensions from SAML2 request namespace + * but SAML2 metadata namespace is required + **/ + //Extensions eidasExtensions = BuilderFactoryUtil.generateExtension(); + + ExtensionsBuilder extensionsBuilder = new ExtensionsBuilder(); + Extensions eidasExtensions = extensionsBuilder.buildObject("urn:oasis:names:tc:SAML:2.0:metadata", "Extensions", "md"); + if (params.getAssuranceLevel() != null) { generateLoA(eidasExtensions); } -- cgit v1.2.3 From 9a1114a1ba64e0236b09c1a9e94b4da7507d3411 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Wed, 16 Nov 2016 16:03:23 +0100 Subject: fix some more bugs in eIDAS SAML-engine * different problems with SAML metadata generation --- .../eidas/utils/MOAeIDASMetadataGenerator.java | 47 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) (limited to 'id/server') diff --git a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java index ab41c2369..dd14972e3 100644 --- a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java +++ b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAeIDASMetadataGenerator.java @@ -53,6 +53,7 @@ import org.opensaml.saml2.metadata.LocalizedString; import org.opensaml.saml2.metadata.NameIDFormat; import org.opensaml.saml2.metadata.Organization; import org.opensaml.saml2.metadata.OrganizationDisplayName; +import org.opensaml.saml2.metadata.OrganizationName; import org.opensaml.saml2.metadata.OrganizationURL; import org.opensaml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml2.metadata.SSODescriptor; @@ -77,6 +78,7 @@ import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import at.gv.egovernment.moa.id.auth.modules.eidas.Constants; +import at.gv.egovernment.moa.id.protocols.pvp2x.utils.SAML2Utils; import eu.eidas.auth.commons.EIDASUtil; import eu.eidas.auth.commons.EidasStringUtil; import eu.eidas.auth.commons.attribute.AttributeDefinition; @@ -127,8 +129,17 @@ public class MOAeIDASMetadataGenerator extends MetadataGenerator { entityDescriptor.setEntityID(params.getEntityID()); entityDescriptor.setOrganization(buildOrganization()); - entityDescriptor.getContactPersons().add(buildContact(ContactPersonTypeEnumeration.SUPPORT)); - entityDescriptor.getContactPersons().add(buildContact(ContactPersonTypeEnumeration.TECHNICAL)); + + /**FIXME: + * HOTFIX: do not add empty contactPerson elements + */ + ContactPerson contactSupport = buildContact(ContactPersonTypeEnumeration.SUPPORT); + if (contactSupport != null) + entityDescriptor.getContactPersons().add(contactSupport); + ContactPerson contactTech = buildContact(ContactPersonTypeEnumeration.TECHNICAL); + if (contactTech != null) + entityDescriptor.getContactPersons().add(contactTech); + entityDescriptor.setValidUntil(getExpireDate()); X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); @@ -165,8 +176,15 @@ public class MOAeIDASMetadataGenerator extends MetadataGenerator { //the node has SP role spSSODescriptor.setWantAssertionsSigned(params.isWantAssertionsSigned()); spSSODescriptor.setAuthnRequestsSigned(true); - spSSODescriptor.setID(idpSSODescriptor == null ? params.getEntityID() - : ("SP" + params.getEntityID())); + + + /**FIXME: + * "SP" + params.getEntityID()) is not a valid XML ID attribute value + */ + //spSSODescriptor.setID(idpSSODescriptor == null ? params.getEntityID() : ("SP" + params.getEntityID())); + spSSODescriptor.setID(SAML2Utils.getSecureIdentifier()); + + if (params.getSPSignature() != null) { spSSODescriptor.setSignature(params.getSPSignature()); } @@ -221,8 +239,13 @@ public class MOAeIDASMetadataGenerator extends MetadataGenerator { SAMLEngineException, EIDASSAMLEngineException { //the node has IDP role idpSSODescriptor.setWantAuthnRequestsSigned(true); - idpSSODescriptor.setID(spSSODescriptor == null ? params.getEntityID() - : ("IDP" + params.getEntityID())); + + /**FIXME: + * "IDP" + params.getEntityID()) is not a valid XML ID attribute value + */ + //idpSSODescriptor.setID(spSSODescriptor == null ? params.getEntityID() : ("IDP" + params.getEntityID())); + idpSSODescriptor.setID(SAML2Utils.getSecureIdentifier()); + if (params.getIDPSignature() != null) { idpSSODescriptor.setSignature(params.getIDPSignature()); } @@ -342,8 +365,16 @@ public class MOAeIDASMetadataGenerator extends MetadataGenerator { private Organization buildOrganization() { Organization organization = null; - try { + try { organization = BuilderFactoryUtil.buildXmlObject(Organization.class); + + /**FIXME: + * set correct OrganizationName value if it is not fixed in next eIDAS node version + */ + OrganizationName orgName = BuilderFactoryUtil.buildXmlObject(OrganizationName.class); + orgName.setName(new LocalizedString(params.getNodeUrl(), "en")); + organization.getOrganizationNames().add(orgName); + OrganizationDisplayName odn = BuilderFactoryUtil.buildXmlObject(OrganizationDisplayName.class); odn.setName(new LocalizedString(params.getCountryName(), "en")); organization.getDisplayNames().add(odn); @@ -374,7 +405,7 @@ public class MOAeIDASMetadataGenerator extends MetadataGenerator { contact = BuilderFactoryUtil.buildXmlObject(ContactPerson.class); if (currentContact == null) { LOGGER.error("ERROR: cannot retrieve contact from the configuration"); - return contact; + return null; } EmailAddress emailAddressObj = BuilderFactoryUtil.buildXmlObject(EmailAddress.class); -- cgit v1.2.3 From 4b9879c26d61ddb24cb0a4be98e8911b62156323 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Fri, 18 Nov 2016 13:03:32 +0100 Subject: fix bug in servlet --- .../gv/egovernment/moa/id/auth/servlet/RedirectServlet.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'id/server') diff --git a/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/auth/servlet/RedirectServlet.java b/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/auth/servlet/RedirectServlet.java index 3eaede028..1848fa6f7 100644 --- a/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/auth/servlet/RedirectServlet.java +++ b/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/auth/servlet/RedirectServlet.java @@ -27,6 +27,7 @@ import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringEscapeUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -70,10 +71,13 @@ public class RedirectServlet { IOAAuthParameters oa = null; String redirectTarget = DEFAULT_REDIRECTTARGET; try { + //validate URL + new java.net.URL(url); + oa = AuthConfigurationProviderFactory.getInstance().getOnlineApplicationParameter(url); String authURL = HTTPUtils.extractAuthURLFromRequest(req); - if (oa == null && !AuthConfigurationProviderFactory.getInstance().getPublicURLPrefix().contains(authURL)) { + if (oa == null || !AuthConfigurationProviderFactory.getInstance().getPublicURLPrefix().contains(authURL)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Parameters not valid"); return; @@ -90,7 +94,7 @@ public class RedirectServlet { } Logger.info("Redirect to " + url); - + if (MiscUtil.isNotEmpty(target)) { // redirectURL = addURLParameter(redirectURL, PARAM_TARGET, // URLEncoder.encode(session.getTarget(), "UTF-8")); @@ -108,7 +112,7 @@ public class RedirectServlet { authURL, DefaultGUIFormBuilderConfiguration.VIEW_REDIRECT, null); - config.putCustomParameter(URL, url); + config.putCustomParameter(URL, StringEscapeUtils.escapeHtml(url)); config.putCustomParameter(TARGET, redirectTarget); guiBuilder.build(resp, config, "RedirectForm.html"); @@ -123,14 +127,13 @@ public class RedirectServlet { resp.setStatus(HttpServletResponse.SC_FOUND); resp.addHeader("Location", url); - } else { Logger.debug("Redirect to " + url); DefaultGUIFormBuilderConfiguration config = new DefaultGUIFormBuilderConfiguration( authURL, DefaultGUIFormBuilderConfiguration.VIEW_REDIRECT, null); - config.putCustomParameter(URL, url); + config.putCustomParameter(URL, StringEscapeUtils.escapeHtml(url)); guiBuilder.build(resp, config, "RedirectForm.html"); } -- cgit v1.2.3 From 55fc69195fd5c0900015b5514f98604ef8ec82c4 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Fri, 18 Nov 2016 13:03:47 +0100 Subject: update SSO session-transfer module --- .../modules/ssotransfer/task/RestoreSSOSessionTask.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) (limited to 'id/server') diff --git a/id/server/modules/moa-id-module-ssoTransfer/src/main/java/at/gv/egovernment/moa/id/auth/modules/ssotransfer/task/RestoreSSOSessionTask.java b/id/server/modules/moa-id-module-ssoTransfer/src/main/java/at/gv/egovernment/moa/id/auth/modules/ssotransfer/task/RestoreSSOSessionTask.java index cf4590fc1..1a216f0df 100644 --- a/id/server/modules/moa-id-module-ssoTransfer/src/main/java/at/gv/egovernment/moa/id/auth/modules/ssotransfer/task/RestoreSSOSessionTask.java +++ b/id/server/modules/moa-id-module-ssoTransfer/src/main/java/at/gv/egovernment/moa/id/auth/modules/ssotransfer/task/RestoreSSOSessionTask.java @@ -184,20 +184,9 @@ public class RestoreSSOSessionTask extends AbstractAuthServletTask { //TODO: implement Signature validation Logger.debug("MobileDevice is valid. --> Starting session reconstruction ..."); - - - //session is valid --> load MOASession object - try { - defaultTaskInitialization(request, executionContext); - - } catch (MOAIDException | MOADatabaseException e1) { - Logger.error("Database Error! MOASession is not stored!"); - throw new TaskExecutionException(pendingReq, "Load MOASession FAILED.", e1); - - } - + //transfer SSO Assertion into MOA-Session - ssoTransferUtils.parseSSOContainerToMOASessionDataObject(pendingReq, moasession, attributeExtractor); + ssoTransferUtils.parseSSOContainerToMOASessionDataObject(pendingReq, pendingReq.getMOASession(), attributeExtractor); // store MOASession into database requestStoreage.storePendingRequest(pendingReq); -- cgit v1.2.3 From 0537b6bf727985bc9d5c075071b52999f01f1975 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Fri, 18 Nov 2016 13:04:17 +0100 Subject: add first parts to support Redirect-Binding for eIDAS Auth. interface --- .../eidas/tasks/GenerateAuthnRequestTask.java | 178 +++++++++++++++------ 1 file changed, 133 insertions(+), 45 deletions(-) (limited to 'id/server') diff --git a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/tasks/GenerateAuthnRequestTask.java b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/tasks/GenerateAuthnRequestTask.java index 3522a16fd..a9c4d5d3a 100644 --- a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/tasks/GenerateAuthnRequestTask.java +++ b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/tasks/GenerateAuthnRequestTask.java @@ -22,10 +22,13 @@ */ package at.gv.egovernment.moa.id.auth.modules.eidas.tasks; +import java.io.ByteArrayOutputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -39,6 +42,7 @@ import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.metadata.EntityDescriptor; import org.opensaml.saml2.metadata.SingleSignOnService; import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.xml.util.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -55,10 +59,12 @@ import at.gv.egovernment.moa.id.auth.modules.eidas.exceptions.EIDASEngineExcepti import at.gv.egovernment.moa.id.auth.modules.eidas.utils.SAMLEngineUtils; import at.gv.egovernment.moa.id.commons.MOAIDAuthConstants; import at.gv.egovernment.moa.id.commons.api.IOAAuthParameters; +import at.gv.egovernment.moa.id.commons.api.IRequest; import at.gv.egovernment.moa.id.commons.api.data.CPEPS; import at.gv.egovernment.moa.id.commons.api.data.StorkAttribute; import at.gv.egovernment.moa.id.commons.api.exceptions.MOAIDException; import at.gv.egovernment.moa.id.process.api.ExecutionContext; +import at.gv.egovernment.moa.id.protocols.pvp2x.utils.SAML2Utils; import at.gv.egovernment.moa.logging.Logger; import at.gv.egovernment.moa.util.MiscUtil; import eu.eidas.auth.commons.EidasStringUtil; @@ -111,15 +117,15 @@ public class GenerateAuthnRequestTask extends AbstractAuthServletTask { // select SingleSignOnService Endpoint from eIDAS-node metadata - String destination = null; + SingleSignOnService authnReqEndpoint = null; String metadataUrl = cpeps.getPepsURL().toString().split(";")[0].trim(); try { EntityDescriptor eIDASNodeMetadata = eIDASMetadataProvider.getEntityDescriptor(metadataUrl); if (eIDASNodeMetadata != null) { SingleSignOnService ssoDescr = selectSingleSignOnServiceFromMetadata(eIDASNodeMetadata); if (ssoDescr != null) { - destination = ssoDescr.getLocation(); - Logger.debug("Use destination URL:" + destination + " from eIDAS metadata:" + metadataUrl); + authnReqEndpoint = ssoDescr; + Logger.debug("Use destination URL:" + authnReqEndpoint.getLocation() + " from eIDAS metadata:" + metadataUrl); } else Logger.warn("eIDAS metadata for node:" + metadataUrl + " has no IDPSSODescriptor or no SingleSignOnService information."); @@ -134,13 +140,21 @@ public class GenerateAuthnRequestTask extends AbstractAuthServletTask { // load SingleSignOnService Endpoint from configuration, if Metadata contains no information // FIXME convenience function for not standard conform metadata - if (MiscUtil.isEmpty(destination)) { + if (authnReqEndpoint == null) { + String destination = null; String[] splitString = cpeps.getPepsURL().toString().split(";"); if (splitString.length > 1) destination = cpeps.getPepsURL().toString().split(";")[1].trim(); - if (MiscUtil.isNotEmpty(destination)) + if (MiscUtil.isNotEmpty(destination)) { Logger.debug("Use eIDAS node destination URL:" + destination + " from configuration"); + + //set POST binding as default binding, if Authn. request endpoint from config is used + authnReqEndpoint = SAML2Utils.createSAMLObject(SingleSignOnService.class); + authnReqEndpoint.setLocation(destination); + authnReqEndpoint.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); + + } else { Logger.error("No eIDAS-node destination URL FOUND. Request eIDAS node not possible."); @@ -193,7 +207,7 @@ public class GenerateAuthnRequestTask extends AbstractAuthServletTask { authnRequestBuilder.providerName(pendingReq.getAuthURL()); String issur = pendingReq.getAuthURL() + Constants.eIDAS_HTTP_ENDPOINT_METADATA; authnRequestBuilder.issuer(issur); - authnRequestBuilder.destination(destination); + authnRequestBuilder.destination(authnReqEndpoint.getLocation()); authnRequestBuilder.nameIdFormat(Constants.eIDAS_REQ_NAMEID_FORMAT); @@ -226,45 +240,21 @@ public class GenerateAuthnRequestTask extends AbstractAuthServletTask { String SAMLRequest = EidasStringUtil.encodeToBase64(token); - //send - try { - VelocityEngine velocityEngine = VelocityProvider.getClassPathVelocityEngine(); - Template template = velocityEngine.getTemplate("/resources/templates/eidas_postbinding_template.vm"); - VelocityContext context = new VelocityContext(); - - String actionType = "SAMLRequest"; - context.put(actionType, SAMLRequest); - Logger.debug("Encoded " + actionType + " original: " + SAMLRequest); - - context.put("RelayState", pendingReq.getRequestID()); - - Logger.debug("Using assertion consumer url as action: " + destination); - context.put("action", destination); - - Logger.debug("Starting template merge"); - StringWriter writer = new StringWriter(); - - Logger.debug("Doing template merge"); - template.merge(context, writer); - Logger.debug("Template merge done"); - - Logger.debug("Sending html content: " + writer.getBuffer().toString()); - - - byte[] content = writer.getBuffer().toString().getBytes("UTF-8"); - response.setContentType(MediaType.HTML_UTF_8.toString()); - response.setContentLength(content.length); - response.getOutputStream().write(content); + if (SAMLConstants.SAML2_POST_BINDING_URI.equals(authnReqEndpoint.getBinding())) + buildPostBindingRequest(pendingReq, authnReqEndpoint, SAMLRequest, authnRequest, response); + + //TODO: redirect Binding is not completely implemented + //else if (SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(authnReqEndpoint.getBinding())) + //buildRedirecttBindingRequest(pendingReq, authnReqEndpoint, token, authnRequest, response); + + else { + Logger.error("eIDAS-node use an unsupported binding (" + + authnReqEndpoint.getBinding() + "). Request eIDAS node not possible."); + throw new MOAIDException("eIDAS.02", new Object[]{"eIDAS-node use an unsupported binding"}); + + } + - revisionsLogger.logEvent(oaConfig, pendingReq, - MOAIDEventConstants.AUTHPROCESS_PEPS_REQUESTED, - authnRequest.getRequest().getId()); - - } catch (Exception e) { - Logger.error("Velocity general error: " + e.getMessage()); - throw new MOAIDException("eIDAS.02", new Object[]{e.getMessage()}, e); - - } }catch (EIDASSAMLEngineException e){ throw new TaskExecutionException(pendingReq, "eIDAS AuthnRequest generation FAILED.", @@ -280,6 +270,102 @@ public class GenerateAuthnRequestTask extends AbstractAuthServletTask { } } + /** + * Encode the eIDAS request with Redirect binding + * + * @param pendingReq + * @param authnReqEndpoint + * @param token + * @param authnRequest + * @param response + * @throws MOAIDException + */ + private void buildRedirecttBindingRequest(IRequest pendingReq, SingleSignOnService authnReqEndpoint, + byte[] token, IRequestMessage authnRequest, HttpServletResponse response) + throws MOAIDException { + + //FIXME: implement correct deflat encoding accodring to SAML2 Redirect Binding specification + + try { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater); + deflaterStream.write(token); + deflaterStream.finish(); + String samlReqBase64 = Base64.encodeBytes(bytesOut.toByteArray(), Base64.DONT_BREAK_LINES); + + + + } catch (Exception e) { + Logger.error("eIDAS Redirect-Binding request encoding error: " + e.getMessage()); + throw new MOAIDException("eIDAS.02", new Object[]{e.getMessage()}, e); + + } + + } + + /** + * Encode the eIDAS request with POST binding + * + * @param pendingReq + * @param authnReqEndpoint + * @param SAMLRequest + * @param authnRequest + * @param response + * @throws MOAIDException + */ + private void buildPostBindingRequest(IRequest pendingReq, SingleSignOnService authnReqEndpoint, + String SAMLRequest, IRequestMessage authnRequest, HttpServletResponse response) + throws MOAIDException { + //send + try { + VelocityEngine velocityEngine = VelocityProvider.getClassPathVelocityEngine(); + Template template = velocityEngine.getTemplate("/resources/templates/eidas_postbinding_template.vm"); + VelocityContext context = new VelocityContext(); + + String actionType = "SAMLRequest"; + context.put(actionType, SAMLRequest); + Logger.debug("Encoded " + actionType + " original: " + SAMLRequest); + + context.put("RelayState", pendingReq.getRequestID()); + + Logger.debug("Using assertion consumer url as action: " + authnReqEndpoint.getLocation()); + context.put("action", authnReqEndpoint.getLocation()); + + Logger.debug("Starting template merge"); + StringWriter writer = new StringWriter(); + + Logger.debug("Doing template merge"); + template.merge(context, writer); + Logger.debug("Template merge done"); + + Logger.debug("Sending html content: " + writer.getBuffer().toString()); + + + byte[] content = writer.getBuffer().toString().getBytes("UTF-8"); + response.setContentType(MediaType.HTML_UTF_8.toString()); + response.setContentLength(content.length); + response.getOutputStream().write(content); + + revisionsLogger.logEvent(pendingReq.getOnlineApplicationConfiguration(), pendingReq, + MOAIDEventConstants.AUTHPROCESS_PEPS_REQUESTED, + authnRequest.getRequest().getId()); + + } catch (Exception e) { + Logger.error("Velocity general error: " + e.getMessage()); + throw new MOAIDException("eIDAS.02", new Object[]{e.getMessage()}, e); + + } + + } + + /** + * Select a SingleSignOnService endPoint from eIDAS node metadata. + * This endPoint receives the Authn. request + * + * @param idpEntity + * @return + */ private SingleSignOnService selectSingleSignOnServiceFromMetadata(EntityDescriptor idpEntity) { //select SingleSignOn Service endpoint from IDP metadata SingleSignOnService endpoint = null; @@ -294,7 +380,9 @@ public class GenerateAuthnRequestTask extends AbstractAuthServletTask { // use POST binding as default if it exists if (sss.getBinding().equals(SAMLConstants.SAML2_POST_BINDING_URI)) endpoint = sss; - + + //TODO: redirect Binding is not completely implemented + // use Redirect binding as backup // else if ( sss.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI) // && endpoint == null ) // endpoint = sss; -- cgit v1.2.3 From 28cf5bd5c149d76b0097d8bbf86a10080ffb75d1 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Tue, 22 Nov 2016 16:06:46 +0100 Subject: fix bug in eIDAS SAML-engine that does not allow SIGNATURE_RSA_SHAxxx_MGF1 algorithms for XML signatures --- .../moa/id/auth/modules/eidas/Constants.java | 2 +- .../id/auth/modules/eidas/config/MOASWSigner.java | 67 ++++++++++++++- .../eidas/utils/MOAWhiteListConfigurator.java | 96 ++++++++++++++++++++++ 3 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAWhiteListConfigurator.java (limited to 'id/server') diff --git a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/Constants.java b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/Constants.java index f45b6ffa5..02c9a8f5d 100644 --- a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/Constants.java +++ b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/Constants.java @@ -45,7 +45,7 @@ public class Constants { public static final String eIDAS_SAML_ENGINE_NAME_ID_CLASS = "class"; //default implementations for eIDAS SAML-engine functionality - public static final String SAML_SIGNING_IMPLENTATION = "eu.eidas.auth.engine.core.impl.SignSW"; + public static final String SAML_SIGNING_IMPLENTATION = "at.gv.egovernment.moa.id.auth.modules.eidas.config.MOASWSigner"; public static final String SAML_ENCRYPTION_IMPLENTATION = "at.gv.egovernment.moa.id.auth.modules.eidas.config.ModifiedEncryptionSW"; //configuration property keys diff --git a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/config/MOASWSigner.java b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/config/MOASWSigner.java index 302c12aaa..5cf5e83ec 100644 --- a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/config/MOASWSigner.java +++ b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/config/MOASWSigner.java @@ -22,12 +22,22 @@ */ package at.gv.egovernment.moa.id.auth.modules.eidas.config; +import java.util.Locale; import java.util.Map; +import org.apache.commons.lang.StringUtils; +import org.apache.xml.security.signature.XMLSignature; +import org.opensaml.xml.signature.SignatureConstants; + +import com.google.common.collect.ImmutableSet; + import at.gv.egovernment.moa.id.auth.modules.eidas.Constants; +import at.gv.egovernment.moa.id.auth.modules.eidas.utils.MOAWhiteListConfigurator; +import at.gv.egovernment.moaspss.logging.Logger; import eu.eidas.auth.engine.configuration.SamlEngineConfigurationException; import eu.eidas.auth.engine.configuration.dom.ConfigurationAdapter; import eu.eidas.auth.engine.configuration.dom.ConfigurationKey; +import eu.eidas.auth.engine.configuration.dom.KeyStoreSignatureConfigurator; import eu.eidas.auth.engine.core.impl.KeyStoreProtocolSigner; import eu.eidas.samlengineconfig.CertificateConfigurationManager; @@ -37,20 +47,71 @@ import eu.eidas.samlengineconfig.CertificateConfigurationManager; */ public class MOASWSigner extends KeyStoreProtocolSigner { + private static Map props; + private ImmutableSet sigAlgWhiteList = null; + + private static final ImmutableSet ALLOWED_ALGORITHMS_FOR_VERIFYING = + ImmutableSet.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256, + SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA384, + SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512, + // RIPEMD is allowed to verify + SignatureConstants.ALGO_ID_SIGNATURE_RSA_RIPEMD160, + SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA256, + SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA384, + SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA512, + + //Set other algorithms which are not supported by openSAML in default + StringUtils.lowerCase(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1_MGF1, Locale.ENGLISH), + StringUtils.lowerCase(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA224_MGF1, Locale.ENGLISH), + StringUtils.lowerCase(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256_MGF1, Locale.ENGLISH), + StringUtils.lowerCase(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA384_MGF1, Locale.ENGLISH), + StringUtils.lowerCase(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512_MGF1, Locale.ENGLISH)); + + private static final ImmutableSet DEFAULT_ALGORITHM_WHITE_LIST = + ImmutableSet.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256, + SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA384, + SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512, + // RIPEMD is not allowed to sign + SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA256, + SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA384, + SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA512, + + //Set other algorithms which are not supported by openSAML in default + StringUtils.lowerCase(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256_MGF1, Locale.ENGLISH)); + public MOASWSigner(Map properties) throws SamlEngineConfigurationException { super(properties); - + props = properties; + } /** * @param configManager * @throws SamlEngineConfigurationException */ - public MOASWSigner(CertificateConfigurationManager configManager) throws SamlEngineConfigurationException { - super(ConfigurationAdapter.adapt(configManager).getInstances().get(Constants.eIDAS_SAML_ENGINE_NAME).getConfigurationEntries().get(ConfigurationKey.SIGNATURE_CONFIGURATION.getKey()).getParameters()); + public MOASWSigner(CertificateConfigurationManager configManager) throws SamlEngineConfigurationException { + super(props = ConfigurationAdapter.adapt(configManager).getInstances().get(Constants.eIDAS_SAML_ENGINE_NAME).getConfigurationEntries().get(ConfigurationKey.SIGNATURE_CONFIGURATION.getKey()).getParameters()); } + @Override + protected ImmutableSet getSignatureAlgorithmWhiteList() { + try { + if (sigAlgWhiteList == null) { + sigAlgWhiteList = MOAWhiteListConfigurator.getAllowedAlgorithms(DEFAULT_ALGORITHM_WHITE_LIST, + ALLOWED_ALGORITHMS_FOR_VERIFYING, + (new KeyStoreSignatureConfigurator().getSignatureConfiguration(props)).getSignatureAlgorithmWhiteList()); + + } + + return sigAlgWhiteList; + + } catch (SamlEngineConfigurationException e) { + Logger.warn("Can not parse eIDAS signing configuration." , e); + return DEFAULT_ALGORITHM_WHITE_LIST; + + } + } } diff --git a/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAWhiteListConfigurator.java b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAWhiteListConfigurator.java new file mode 100644 index 000000000..7d647ff15 --- /dev/null +++ b/id/server/modules/moa-id-module-eIDAS/src/main/java/at/gv/egovernment/moa/id/auth/modules/eidas/utils/MOAWhiteListConfigurator.java @@ -0,0 +1,96 @@ +/* + * Copyright 2014 Federal Chancellery Austria + * MOA-ID has been developed in a cooperation between BRZ, the Federal + * Chancellery Austria - ICT staff unit, and Graz University of Technology. + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * http://www.osor.eu/eupl/ + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ +package at.gv.egovernment.moa.id.auth.modules.eidas.utils; + +import java.util.Locale; +import java.util.regex.Pattern; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; + +import com.google.common.collect.ImmutableSet; + +import at.gv.egovernment.moa.id.commons.utils.KeyValueUtils; + +/** + * @author tlenz + * + */ +public class MOAWhiteListConfigurator { + private static final Pattern WHITE_LIST_SPLITTER = Pattern.compile("[;,]"); + + + public static ImmutableSet getAllowedAlgorithms( ImmutableSet defaultWhiteList, + ImmutableSet allowedValues, + String algorithmWhiteListValue) { + if (StringUtils.isBlank(algorithmWhiteListValue)) { + return defaultWhiteList; + } + ImmutableSet.Builder allowed = ImmutableSet.builder(); + String[] wlAlgorithms = WHITE_LIST_SPLITTER.split(algorithmWhiteListValue); + if (null != wlAlgorithms && wlAlgorithms.length > 0) { + return getAllowedAlgorithms(defaultWhiteList, allowedValues, ImmutableSet.copyOf(wlAlgorithms)); + } + return defaultWhiteList; + } + + + public static ImmutableSet getAllowedAlgorithms( ImmutableSet defaultWhiteList, + ImmutableSet allowedValues, + ImmutableSet candidateValues) { + if (CollectionUtils.isEmpty(candidateValues)) { + return defaultWhiteList; + } + ImmutableSet.Builder allowed = ImmutableSet.builder(); + boolean modified = false; + for (String candidateValue : candidateValues) { + + /**FIX: + * fix problem with lowerCase and MGF1 signature algorithms + * + */ + candidateValue = StringUtils.trimToNull( + KeyValueUtils.removeAllNewlineFromString(candidateValue)); + if (StringUtils.isNotBlank(candidateValue)) { + String candidateAlgorithm = StringUtils.lowerCase(candidateValue, Locale.ENGLISH); + if (allowedValues.contains(candidateAlgorithm)) { + allowed.add(candidateValue); + if (!modified && !candidateAlgorithm.equals(candidateValue)) { + modified = true; + } + } else { + modified = true; + } + } + } + if (!modified) { + return candidateValues; + } + ImmutableSet set = allowed.build(); + if (set.isEmpty()) { + return defaultWhiteList; + } + return set; + } + +} -- cgit v1.2.3 From 889361a1a38078e7533b22a2419ead066577bf8f Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Thu, 24 Nov 2016 06:48:12 +0100 Subject: fix check if ELGA mandate-service module is configured --- .../id/auth/modules/elgamandates/ELGAMandatesAuthModuleImpl.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'id/server') diff --git a/id/server/modules/moa-id-module-elga_mandate_service/src/main/java/at/gv/egovernment/moa/id/auth/modules/elgamandates/ELGAMandatesAuthModuleImpl.java b/id/server/modules/moa-id-module-elga_mandate_service/src/main/java/at/gv/egovernment/moa/id/auth/modules/elgamandates/ELGAMandatesAuthModuleImpl.java index c2efe5bfc..f14ffb111 100644 --- a/id/server/modules/moa-id-module-elga_mandate_service/src/main/java/at/gv/egovernment/moa/id/auth/modules/elgamandates/ELGAMandatesAuthModuleImpl.java +++ b/id/server/modules/moa-id-module-elga_mandate_service/src/main/java/at/gv/egovernment/moa/id/auth/modules/elgamandates/ELGAMandatesAuthModuleImpl.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import at.gv.egovernment.moa.id.auth.modules.internal.DefaultCitizenCardAuthModuleImpl; import at.gv.egovernment.moa.id.commons.api.AuthConfiguration; +import at.gv.egovernment.moa.id.commons.config.MOAIDConfigurationConstants; import at.gv.egovernment.moa.id.process.api.ExecutionContext; import at.gv.egovernment.moa.util.MiscUtil; @@ -56,8 +57,9 @@ public class ELGAMandatesAuthModuleImpl extends DefaultCitizenCardAuthModuleImpl //check if BKU authentication is selected and ELGA-MandateService is configurated if (MiscUtil.isNotEmpty(selectedProcessID)) { - if (MiscUtil.isNotEmpty(authConfig.getBasicMOAIDConfiguration( - ELGAMandatesAuthConstants.CONFIG_PROPS_ENTITYID))) + if (MiscUtil.isNotEmpty(authConfig.getConfigurationWithKey( + MOAIDConfigurationConstants.PREFIX_MOAID_GENERAL + "." + + ELGAMandatesAuthConstants.CONFIG_PROPS_ENTITYID))) return "DefaultAuthenticationWithELGAMandates"; } -- cgit v1.2.3 From 1ee287cf9d5aa2a2199724252bc331e851eb3669 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Fri, 25 Nov 2016 07:45:10 +0100 Subject: fix problem in SAML2 credential provider that selects a wrong keyStore --- .../moa/id/protocols/pvp2x/signer/AbstractCredentialProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'id/server') diff --git a/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/protocols/pvp2x/signer/AbstractCredentialProvider.java b/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/protocols/pvp2x/signer/AbstractCredentialProvider.java index 77cc7228b..df4866c30 100644 --- a/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/protocols/pvp2x/signer/AbstractCredentialProvider.java +++ b/id/server/idserverlib/src/main/java/at/gv/egovernment/moa/id/protocols/pvp2x/signer/AbstractCredentialProvider.java @@ -41,7 +41,7 @@ import at.gv.egovernment.moa.util.MiscUtil; public abstract class AbstractCredentialProvider { - private static KeyStore keyStore = null; + private KeyStore keyStore = null; /** * Get a friendlyName for this keyStore implementation -- cgit v1.2.3 From 2f872e5cec51738ff6e8c3dec5cd25455afeea26 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Thu, 1 Dec 2016 14:16:04 +0100 Subject: fix possible problem with Velocity Engine internal logging --- .../egovernment/moa/id/auth/frontend/velocity/VelocityProvider.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'id/server') diff --git a/id/server/moa-id-frontend-resources/src/main/java/at/gv/egovernment/moa/id/auth/frontend/velocity/VelocityProvider.java b/id/server/moa-id-frontend-resources/src/main/java/at/gv/egovernment/moa/id/auth/frontend/velocity/VelocityProvider.java index 022c144f0..21fe110ca 100644 --- a/id/server/moa-id-frontend-resources/src/main/java/at/gv/egovernment/moa/id/auth/frontend/velocity/VelocityProvider.java +++ b/id/server/moa-id-frontend-resources/src/main/java/at/gv/egovernment/moa/id/auth/frontend/velocity/VelocityProvider.java @@ -50,6 +50,7 @@ */ package at.gv.egovernment.moa.id.auth.frontend.velocity; +import org.apache.velocity.app.Velocity; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.runtime.RuntimeConstants; @@ -104,8 +105,9 @@ public class VelocityProvider { VelocityEngine velocityEngine = new VelocityEngine(); velocityEngine.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8"); velocityEngine.setProperty(RuntimeConstants.OUTPUT_ENCODING, "UTF-8"); - velocityEngine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, - "org.apache.velocity.runtime.log.SimpleLog4JLogSystem"); +// velocityEngine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, +// "org.apache.velocity.runtime.log.SimpleLog4JLogSystem"); + velocityEngine.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM, new VelocityLogAdapter() ); return velocityEngine; } -- cgit v1.2.3 From b43a2503c27d51bdac0d7b75d73aca461075530f Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Fri, 9 Dec 2016 09:42:28 +0100 Subject: fix bug in OpenID protocol implementation that generates a wrong encoded error response --- .../moa/id/protocols/oauth20/protocol/OAuth20Protocol.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'id/server') diff --git a/id/server/modules/moa-id-module-openID/src/main/java/at/gv/egovernment/moa/id/protocols/oauth20/protocol/OAuth20Protocol.java b/id/server/modules/moa-id-module-openID/src/main/java/at/gv/egovernment/moa/id/protocols/oauth20/protocol/OAuth20Protocol.java index 118c53f6b..75ea41449 100644 --- a/id/server/modules/moa-id-module-openID/src/main/java/at/gv/egovernment/moa/id/protocols/oauth20/protocol/OAuth20Protocol.java +++ b/id/server/modules/moa-id-module-openID/src/main/java/at/gv/egovernment/moa/id/protocols/oauth20/protocol/OAuth20Protocol.java @@ -204,9 +204,11 @@ public class OAuth20Protocol extends AbstractAuthProtocolModulController { OAuth20Util.addParameterToURL(url, OAuth20Constants.PARAM_ERROR, errorCode); OAuth20Util.addParameterToURL(url, OAuth20Constants.PARAM_ERROR_DESCRIPTION, errorDescription); - if (MiscUtil.isNotEmpty(moaError)) - OAuth20Util.addParameterToURL(url, OAuth20Constants.PARAM_ERROR_URI, errorUri + "#" + moaError); OAuth20Util.addParameterToURL(url, OAuth20Constants.PARAM_STATE, state); + if (MiscUtil.isNotEmpty(moaError)) + OAuth20Util.addParameterToURL(url, OAuth20Constants.PARAM_ERROR_URI, + URLEncoder.encode(errorUri + "#" + moaError, "UTF-8")); + response.setContentType("text/html"); response.setStatus(HttpServletResponse.SC_FOUND); @@ -220,7 +222,8 @@ public class OAuth20Protocol extends AbstractAuthProtocolModulController { Map params = new HashMap(); params.put(OAuth20Constants.PARAM_ERROR, errorCode); params.put(OAuth20Constants.PARAM_ERROR_DESCRIPTION, errorDescription); - params.put(OAuth20Constants.PARAM_ERROR_URI, errorUri + "#" + moaError); + params.put(OAuth20Constants.PARAM_ERROR_URI, + URLEncoder.encode(errorUri + "#" + moaError, "UTF-8")); // create response JsonObject jsonObject = new JsonObject(); -- cgit v1.2.3 From 71ba131f39e23c2f77474f543a206736ba623d03 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Mon, 12 Dec 2016 14:26:19 +0100 Subject: update handbook --- id/server/doc/handbook/protocol/protocol.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'id/server') diff --git a/id/server/doc/handbook/protocol/protocol.html b/id/server/doc/handbook/protocol/protocol.html index 5a578a5aa..7d3f8d627 100644 --- a/id/server/doc/handbook/protocol/protocol.html +++ b/id/server/doc/handbook/protocol/protocol.html @@ -1295,8 +1295,8 @@ https://<host>:<port>/moa-id-auth/pvp2/metadata

Dieser Abschnitt beschreibt die einzelnen OpenID Connect spezifischen Nachrichten, welche zwischen der Online-Applikation und dem Modul MOA-ID-Auth während eines Authentifizierungsvorgangs ausgetauscht werden. Hierbei wird auch auf das Sequenzdiagramm aus Abschnitt 3.1 Bezug genommen.

3.2.1 AuthCode Request

-

Der AuthCode Request ist die Authentifizierungsanfrage einer Online-Applikation für eine Benutzerin oder einen Benutzer. -Folgende Parameter müssen mit dem AuthCode-Request mitgesendet werden, wobei für die Übertragung der Parameter sowohl http GET als auch http POST verwendet werden kann.

+

Der AuthCode Request ist die Authentifizierungsanfrage einer Online-Applikation für eine Benutzerin oder einen Benutzer. +Folgende Parameter müssen mit dem AuthCode-Request mitgesendet werden, wobei für die Übertragung der Parameter sowohl http GET als auch http POST verwendet werden kann.

@@ -1498,6 +1498,12 @@ Folgende Parameter müssen mit dem AuthCode-Request mitgesendet werden, wobei f + + + + +
Namehttps://demo.egiz.gv.at/demoportal_moaid-2.0/moa_errorcodes.html#1000 URL auf eine Seite mit zusätzlicher Fehlerbeschreibung
state1425782214234

Der von der Online-Applikation generierte und im AuthCode Request übergebene CSRF Token.

+

Hinweis: Dieser Parameter wird nur dann returniert wenn dieser auch im vorrangegangenen Request (z.B. AuthCode Request) angegeben wurde.

 

3 SAML 1

-- cgit v1.2.3