diff options
author | Thomas Lenz <tlenz@iaik.tugraz.at> | 2018-06-05 10:46:41 +0200 |
---|---|---|
committer | Thomas Lenz <tlenz@iaik.tugraz.at> | 2018-06-05 10:46:41 +0200 |
commit | cd5cef47db73c85cbb2defdec3b283655fdc859b (patch) | |
tree | 97a3f3ba121babcc4850b5048e63aee831399676 /id/server/modules/moa-id-module-sl20_authentication/src/main/java | |
parent | 84a55fe8bec3924102bd2217f7e39e7a698f2829 (diff) | |
download | moa-id-spss-cd5cef47db73c85cbb2defdec3b283655fdc859b.tar.gz moa-id-spss-cd5cef47db73c85cbb2defdec3b283655fdc859b.tar.bz2 moa-id-spss-cd5cef47db73c85cbb2defdec3b283655fdc859b.zip |
update SL20 implementation
Diffstat (limited to 'id/server/modules/moa-id-module-sl20_authentication/src/main/java')
10 files changed, 256 insertions, 161 deletions
diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/SL20AuthenticationModulImpl.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/SL20AuthenticationModulImpl.java index a4044ce21..367e7b604 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/SL20AuthenticationModulImpl.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/SL20AuthenticationModulImpl.java @@ -22,6 +22,9 @@ */ package at.gv.egovernment.moa.id.auth.modules.sl20_auth; +import java.util.Arrays; +import java.util.List; + import javax.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; @@ -38,10 +41,10 @@ import at.gv.egovernment.moa.logging.Logger; * @author tlenz * */ -public class SL20AuthenticationModulImpl implements AuthModule { - +public class SL20AuthenticationModulImpl implements AuthModule { private int priority = 3; - + public static final List<String> VDA_TYPE_IDS = Arrays.asList("1", "2", "3", "4"); + @Autowired(required=true) protected AuthConfiguration authConfig; @Autowired(required=true) private AuthenticationManager authManager; @@ -62,6 +65,7 @@ public class SL20AuthenticationModulImpl implements AuthModule { protected void initalSL20Authentication() { //parameter to whiteList authManager.addHeaderNameToWhiteList(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE); + authManager.addHeaderNameToWhiteList(SL20Constants.HTTP_HEADER_SL20_VDA_TYPE); } @@ -71,17 +75,23 @@ public class SL20AuthenticationModulImpl implements AuthModule { */ @Override public String selectProcess(ExecutionContext context) { - if (StringUtils.isNotBlank((String) context.get(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE.toLowerCase())) || - StringUtils.isNotBlank((String) context.get(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE))) { - Logger.trace(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE + "' header found"); + String sl20ClientTypeHeader = (String) context.get(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE.toLowerCase()); + String sl20VDATypeHeader = (String) context.get(SL20Constants.HTTP_HEADER_SL20_VDA_TYPE.toLowerCase()); + + if ( StringUtils.isNotBlank(sl20ClientTypeHeader) +// && ( +// StringUtils.isNotBlank(sl20VDATypeHeader) +// //&& VDA_TYPE_IDS.contains(sl20VDATypeHeader.trim()) +// ) + ) { + Logger.trace(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE + "' header found"); return "SL20Authentication"; } else { Logger.trace("No '" + SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE + "' header found"); return null; - } - + } } /* (non-Javadoc) diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/JsonSecurityUtils.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/JsonSecurityUtils.java index 2563c7f7d..c95e0b731 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/JsonSecurityUtils.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/JsonSecurityUtils.java @@ -1,9 +1,11 @@ package at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20; +import java.io.IOException; import java.security.Key; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; @@ -36,6 +38,7 @@ import at.gv.egovernment.moa.id.auth.modules.sl20_auth.exceptions.SLCommandoPars import at.gv.egovernment.moa.id.commons.api.AuthConfiguration; import at.gv.egovernment.moa.id.commons.utils.X509Utils; import at.gv.egovernment.moa.logging.Logger; +import at.gv.egovernment.moa.util.Base64Utils; import at.gv.egovernment.moa.util.FileUtils; import at.gv.egovernment.moa.util.KeyStoreUtils; import at.gv.egovernment.moa.util.MiscUtil; @@ -143,8 +146,11 @@ public class JsonSecurityUtils implements IJOSETools{ //set signing information jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); jws.setKey(signPrivKey); - jws.setCertificateChainHeaderValue(signCertChain); - + + //TODO: + //jws.setCertificateChainHeaderValue(signCertChain); + jws.setX509CertSha256ThumbprintHeaderValue(signCertChain[0]); + return jws.getCompactSerialization(); } catch (JoseException e) { @@ -181,6 +187,11 @@ public class JsonSecurityUtils implements IJOSETools{ } else { Logger.info("Can NOT find JOSE certificate in truststore."); Logger.debug("JOSE certificate: " + sortedX5cCerts.get(0).toString()); + try { + Logger.debug("Cert: " + Base64Utils.encode(sortedX5cCerts.get(0).getEncoded())); + } catch (CertificateEncodingException | IOException e) { + e.printStackTrace(); + } } diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20Constants.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20Constants.java index 33bb4fe36..658384578 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20Constants.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20Constants.java @@ -8,7 +8,7 @@ import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers; import org.jose4j.jws.AlgorithmIdentifiers; public class SL20Constants { - public static final String CURRENT_SL20_VERSION = "10"; + public static final int CURRENT_SL20_VERSION = 10; //http binding parameters public static final String PARAM_SL20_REQ_COMMAND_PARAM = "slcommand"; @@ -18,6 +18,7 @@ public class SL20Constants { public static final String PARAM_SL20_REQ_TRANSACTIONID = "slTransactionID"; public static final String HTTP_HEADER_SL20_CLIENT_TYPE = "SL2ClientType"; + public static final String HTTP_HEADER_SL20_VDA_TYPE = "X-MOA-VDA"; public static final String HTTP_HEADER_VALUE_NATIVE = "nativeApp"; @@ -129,8 +130,9 @@ public class SL20Constants { public static final String SL20_COMMAND_PARAM_EID_DATAURL = SL20_COMMAND_PARAM_GENERAL_DATAURL; public static final String SL20_COMMAND_PARAM_EID_ATTRIBUTES = "attributes"; public static final String SL20_COMMAND_PARAM_EID_ATTRIBUTES_MANDATEREFVALUE = "MANDATE-REFERENCE-VALUE"; - public static final String SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPUNIQUEID = "SP-FRIENDLYNAME"; - public static final String SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPFRIENDLYNAME = "SP-UNIQUEID"; + public static final String SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPUNIQUEID = "SP-UNIQUEID"; + public static final String SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPFRIENDLYNAME = "SP-FRIENDLYNAME"; + public static final String SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPCOUNTRYCODE = "SP-COUNTRYCODE"; public static final String SL20_COMMAND_PARAM_EID_X5CENC = SL20_COMMAND_PARAM_GENERAL_RESPONSEENCRYPTIONCERTIFICATE; public static final String SL20_COMMAND_PARAM_EID_RESULT_IDL = "EID-IDENTITY-LINK"; public static final String SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK = "EID-AUTH-BLOCK"; diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20HttpBindingUtils.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20HttpBindingUtils.java index cc7137a0f..169cb8e73 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20HttpBindingUtils.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20HttpBindingUtils.java @@ -2,7 +2,6 @@ package at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20; import java.io.IOException; import java.io.StringWriter; -import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import javax.servlet.http.HttpServletRequest; @@ -10,6 +9,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; +import org.jose4j.base64url.Base64Url; import com.google.gson.JsonObject; @@ -33,7 +33,9 @@ public class SL20HttpBindingUtils { } else { Logger.debug("Client request containts is no native client ... "); URIBuilder clientRedirectURI = new URIBuilder(redirectURL); - clientRedirectURI.addParameter(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM, sl20Forward.toString()); + clientRedirectURI.addParameter( + SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM, + Base64Url.encode(sl20Forward.toString().getBytes())); response.setStatus(307); response.setHeader("Location", clientRedirectURI.build().toString()); diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONBuilderUtils.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONBuilderUtils.java index 52d7e1e67..d5dec1fe1 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONBuilderUtils.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONBuilderUtils.java @@ -387,7 +387,7 @@ public class SL20JSONBuilderUtils { */ public static JsonObject createGenericRequest(String reqId, String transactionId, JsonElement payLoad, String signedPayload) throws SLCommandoBuildException { JsonObject req = new JsonObject(); - addSingleStringElement(req, SL20Constants.SL20_VERSION, SL20Constants.CURRENT_SL20_VERSION, true); + addSingleIntegerElement(req, SL20Constants.SL20_VERSION, SL20Constants.CURRENT_SL20_VERSION, true); addSingleStringElement(req, SL20Constants.SL20_REQID, reqId, true); addSingleStringElement(req, SL20Constants.SL20_TRANSACTIONID, transactionId, false); addOnlyOnceOfTwo(req, SL20Constants.SL20_PAYLOAD, SL20Constants.SL20_SIGNEDPAYLOAD, @@ -411,7 +411,7 @@ public class SL20JSONBuilderUtils { JsonElement payLoad, String signedPayload) throws SLCommandoBuildException { JsonObject req = new JsonObject(); - addSingleStringElement(req, SL20Constants.SL20_VERSION, SL20Constants.CURRENT_SL20_VERSION, true); + addSingleIntegerElement(req, SL20Constants.SL20_VERSION, SL20Constants.CURRENT_SL20_VERSION, true); addSingleStringElement(req, SL20Constants.SL20_RESPID, respId, true); addSingleStringElement(req, SL20Constants.SL20_INRESPTO, inResponseTo, true); addSingleStringElement(req, SL20Constants.SL20_TRANSACTIONID, transactionId, false); @@ -568,6 +568,17 @@ public class SL20JSONBuilderUtils { } + private static void addSingleIntegerElement(JsonObject parent, String keyId, Integer value, boolean isRequired) throws SLCommandoBuildException { + validateParentAndKey(parent, keyId); + + if (isRequired && value == null) + throw new SLCommandoBuildException(keyId + " has an empty value"); + + else if (value != null) + parent.addProperty(keyId, value); + + } + private static void addSingleJSONElement(JsonObject parent, String keyId, JsonElement element, boolean isRequired) throws SLCommandoBuildException { validateParentAndKey(parent, keyId); diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONExtractorUtils.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONExtractorUtils.java index 6949b7a18..2e81d9c64 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONExtractorUtils.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/SL20JSONExtractorUtils.java @@ -1,7 +1,6 @@ package at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20; import java.io.InputStreamReader; -import java.net.URLDecoder; import java.util.Base64; import java.util.HashMap; import java.util.Iterator; @@ -13,6 +12,7 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.utils.URIBuilder; import org.apache.log4j.Logger; +import org.jose4j.base64url.Base64Url; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -107,45 +107,64 @@ public class SL20JSONExtractorUtils { } /** - * Extract Map of Key/Value pairs from a JSON Array + * Extract Map of Key/Value pairs from a JSON Element * - * @param input - * @param keyID + * @param input parent JSON object + * @param keyID KeyId of the child that should be parsed * @param isRequired * @return * @throws SLCommandoParserException */ public static Map<String, String> getMapOfStringElements(JsonObject input, String keyID, boolean isRequired) throws SLCommandoParserException { JsonElement internal = getAndCheck(input, keyID, isRequired); + return getMapOfStringElements(internal); + } + + /** + * Extract Map of Key/Value pairs from a JSON Element + * + * @param input + * @return + * @throws SLCommandoParserException + */ + public static Map<String, String> getMapOfStringElements(JsonElement input) throws SLCommandoParserException { Map<String, String> result = new HashMap<String, String>(); - if (internal != null) { - if (!internal.isJsonArray()) - throw new SLCommandoParserException("JSON Element IS NOT a JSON array"); - - Iterator<JsonElement> arrayIterator = internal.getAsJsonArray().iterator(); - while(arrayIterator.hasNext()) { - //JsonObject next = arrayIterator.next().getAsJsonObject(); - //result.put( - // next.get(SL20Constants.SL20_COMMAND_PARAM_GENERAL_REQPARAMETER_KEY).getAsString(), - // next.get(SL20Constants.SL20_COMMAND_PARAM_GENERAL_REQPARAMETER_VALUE).getAsString()); - JsonElement next = arrayIterator.next(); - Iterator<Entry<String, JsonElement>> entry = next.getAsJsonObject().entrySet().iterator(); - while (entry.hasNext()) { - Entry<String, JsonElement> el = entry.next(); - if (result.containsKey(el.getKey())) - log.info("Attr. Map already contains Element with Key: " + el.getKey() + ". Overwrite element ... "); - - result.put(el.getKey(), el.getValue().getAsString()); + if (input != null) { + if (input.isJsonArray()) { + Iterator<JsonElement> arrayIterator = input.getAsJsonArray().iterator(); + while(arrayIterator.hasNext()) { + JsonElement next = arrayIterator.next(); + Iterator<Entry<String, JsonElement>> entry = next.getAsJsonObject().entrySet().iterator(); + entitySetToMap(result, entry); - } - } + } + + } else if (input.isJsonObject()) { + Iterator<Entry<String, JsonElement>> objectKeys = input.getAsJsonObject().entrySet().iterator(); + entitySetToMap(result, objectKeys); + + } else + throw new SLCommandoParserException("JSON Element IS NOT a JSON array or a JSON object"); + } return result; } + private static void entitySetToMap(Map<String, String> result, Iterator<Entry<String, JsonElement>> entry) { + while (entry.hasNext()) { + Entry<String, JsonElement> el = entry.next(); + if (result.containsKey(el.getKey())) + log.info("Attr. Map already contains Element with Key: " + el.getKey() + ". Overwrite element ... "); + + result.put(el.getKey(), el.getValue().getAsString()); + + } + + } + public static JsonElement extractSL20Result(JsonObject command, IJOSETools decrypter, boolean mustBeEncrypted) throws SL20Exception { JsonElement result = command.get(SL20Constants.SL20_COMMAND_CONTAINER_RESULT); @@ -207,19 +226,21 @@ public class SL20JSONExtractorUtils { if (sl20Payload == null && sl20SignedPayload == null) throw new SLCommandoParserException("NO payLoad OR signedPayload FOUND."); - else if (sl20Payload == null && sl20SignedPayload == null) - throw new SLCommandoParserException("payLoad AND signedPayload FOUND. Can not used twice"); - + //TODO: + //else if (sl20Payload != null && sl20SignedPayload != null) { + //log.warn("Find 'signed' AND 'unsigned' SL2.0 payload"); + //throw new SLCommandoParserException("payLoad AND signedPayload FOUND. Can not used twice"); + //} else if (sl20SignedPayload == null && mustBeSigned) throw new SLCommandoParserException("payLoad MUST be signed."); + + else if (joseTools != null && sl20SignedPayload != null && sl20SignedPayload.isJsonPrimitive()) { + return joseTools.validateSignature(sl20SignedPayload.getAsString()); - else if (sl20Payload != null) + } else if (sl20Payload != null) return new VerificationResult(sl20Payload.getAsJsonObject()); - else if (sl20SignedPayload != null && sl20SignedPayload.isJsonPrimitive()) { - return joseTools.validateSignature(sl20SignedPayload.getAsString()); - - } else + else throw new SLCommandoParserException("Internal build error"); @@ -242,10 +263,10 @@ public class SL20JSONExtractorUtils { throw new SLCommandoParserException("Find Redirect statuscode but not Location header"); String sl20RespString = new URIBuilder(locationHeader[0].getValue()).getQueryParams().get(0).getValue(); - sl20Resp = new JsonParser().parse(URLDecoder.decode(sl20RespString)).getAsJsonObject(); + sl20Resp = new JsonParser().parse(Base64Url.encode((sl20RespString.getBytes()))).getAsJsonObject(); } else if (httpResp.getStatusLine().getStatusCode() == 200) { - if (!httpResp.getEntity().getContentType().getValue().equals("application/json;charset=UTF-8")) + if (!httpResp.getEntity().getContentType().getValue().startsWith("application/json")) throw new SLCommandoParserException("SL20 response with a wrong ContentType: " + httpResp.getEntity().getContentType().getValue()); sl20Resp = parseSL20ResultFromResponse(httpResp.getEntity()); diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/verifier/QualifiedeIDVerifier.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/verifier/QualifiedeIDVerifier.java index d07d7a78a..a7253c2c6 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/verifier/QualifiedeIDVerifier.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/sl20/verifier/QualifiedeIDVerifier.java @@ -1,14 +1,22 @@ package at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20.verifier; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.Arrays; import java.util.Date; import java.util.List; import org.jaxen.SimpleNamespaceContext; +import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.xml.XMLObject; +import org.opensaml.xml.io.Unmarshaller; +import org.opensaml.xml.io.UnmarshallerFactory; import org.w3c.dom.Element; +import org.xml.sax.SAXException; import at.gv.egovernment.moa.id.auth.builder.SignatureVerificationUtils; -import at.gv.egovernment.moa.id.auth.exception.ValidateException; import at.gv.egovernment.moa.id.auth.invoke.SignatureVerificationInvoker; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.exceptions.SL20eIDDataValidationException; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20.SL20Constants; @@ -22,10 +30,12 @@ import at.gv.egovernment.moa.id.commons.api.data.IIdentityLink; import at.gv.egovernment.moa.id.commons.api.data.IVerifiyXMLSignatureResponse; import at.gv.egovernment.moa.id.commons.api.exceptions.MOAIDException; import at.gv.egovernment.moa.id.protocols.pvp2x.utils.AssertionAttributeExtractor; +import at.gv.egovernment.moa.id.protocols.pvp2x.utils.SAML2Utils; import at.gv.egovernment.moa.logging.Logger; import at.gv.egovernment.moa.sig.tsl.utils.MiscUtil; import at.gv.egovernment.moa.util.Base64Utils; import at.gv.egovernment.moa.util.Constants; +import at.gv.egovernment.moa.util.DOMUtils; public class QualifiedeIDVerifier { @@ -69,21 +79,22 @@ public class QualifiedeIDVerifier { verifyXMLSignatureResponse, authConfig.getIdentityLinkX509SubjectNames(), VerifyXMLSignatureResponseValidator.CHECK_IDENTITY_LINK, - oaParam); + oaParam, + authConfig); } public static IVerifiyXMLSignatureResponse verifyAuthBlock(String authBlockB64, IOAAuthParameters oaParam, AuthConfiguration authConfig) throws MOAIDException, IOException { String trustProfileId = authConfig.getMoaSpAuthBlockTrustProfileID(oaParam.isUseAuthBlockTestTestStore()); - List<String> verifyTransformsInfoProfileID = null; + List<String> verifyTransformsInfoProfileID = Arrays.asList("SL20Authblock_v1.0"); SignatureVerificationUtils sigVerify = new SignatureVerificationUtils(); IVerifiyXMLSignatureResponse sigVerifyResult = sigVerify.verify(Base64Utils.decode(authBlockB64, false), trustProfileId , verifyTransformsInfoProfileID); // validates the <VerifyXMLSignatureResponse> VerifyXMLSignatureResponseValidator.getInstance().validate(sigVerifyResult, - null, VerifyXMLSignatureResponseValidator.CHECK_AUTH_BLOCK, oaParam); + null, VerifyXMLSignatureResponseValidator.CHECK_AUTH_BLOCK, oaParam, authConfig); return sigVerifyResult; @@ -120,7 +131,7 @@ public class QualifiedeIDVerifier { // date and time validateSigningDateTime(sigVerifyResult, authBlockExtractor); - } catch ( ValidateException e) { + } catch ( Exception e) { Logger.warn("Validation of eID information FAILED. ", e); throw new SL20eIDDataValidationException(new Object[] { SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_IDL, @@ -134,6 +145,59 @@ public class QualifiedeIDVerifier { } + public static Assertion parseAuthBlockToSaml2Assertion(String authblockB64) throws SL20eIDDataValidationException { + try { + //parse authBlock into SAML2 Assertion + byte[] authBlockBytes = Base64Utils.decode(authblockB64, false); + Element authBlockDOM = DOMUtils.parseXmlValidating(new ByteArrayInputStream(authBlockBytes)); + + //A-Trust workarounda +// Element authBlockDOM = DOMUtils.parseXmlValidating(new ByteArrayInputStream(authblockB64.getBytes())); +// Element authBlockDOM = DOMUtils.parseXmlNonValidating(new ByteArrayInputStream(authblockB64.getBytes())); + + DefaultBootstrap.bootstrap(); + UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(authBlockDOM); + XMLObject samlAssertion = unmarshaller.unmarshall(authBlockDOM); + + //validate SAML2 Assertion + SAML2Utils.schemeValidation(samlAssertion); + + if (samlAssertion instanceof Assertion) + return (Assertion) samlAssertion; + else + throw new SL20eIDDataValidationException( + new Object[] { + SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, + "AuthBlock is NOT of type SAML2 Assertion" + }); + + } catch (SL20eIDDataValidationException e) { + throw e; + + } catch (SAXException e) { + Logger.info("Scheme validation of SAML2 AuthBlock FAILED. Reason: " + e.getMessage()); + throw new SL20eIDDataValidationException( + new Object[] { + SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, + e.getMessage() + }, + e); + + } catch (Exception e) { + Logger.info("Can not parse AuthBlock. Reason: " + e.getMessage()); + Logger.trace("FullAuthBlock: " + authblockB64); + throw new SL20eIDDataValidationException( + new Object[] { + SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, + e.getMessage() + }, + e); + + } + + } + private static void validateSigningDateTime( IVerifiyXMLSignatureResponse sigVerifyResult, AssertionAttributeExtractor authBlockExtractor) throws SL20eIDDataValidationException { Date signingDate = sigVerifyResult.getSigningDateTime(); Date notBefore = authBlockExtractor.getAssertionNotBefore(); @@ -163,7 +227,7 @@ public class QualifiedeIDVerifier { + " NotBefore:" + notBefore.toString() + " NotOrNotAfter:" + notOrNotAfter.toString()); - if (signingDate.after(notBefore) || signingDate.before(notOrNotAfter)) + if (signingDate.after(notBefore) && signingDate.before(notOrNotAfter)) Logger.debug("Signing date validation successfull"); else { diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/CreateQualeIDRequestTask.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/CreateQualeIDRequestTask.java index 763454639..26283cab2 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/CreateQualeIDRequestTask.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/CreateQualeIDRequestTask.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import javax.net.ssl.SSLSocketFactory; import javax.servlet.http.HttpServletRequest; @@ -17,6 +16,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; +import org.jose4j.base64url.Base64Url; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -39,7 +39,6 @@ import at.gv.egovernment.moa.id.commons.api.AuthConfiguration; import at.gv.egovernment.moa.id.commons.api.IOAAuthParameters; import at.gv.egovernment.moa.id.commons.api.exceptions.MOAIDException; import at.gv.egovernment.moa.id.commons.utils.HttpClientWithProxySupport; -import at.gv.egovernment.moa.id.commons.utils.KeyValueUtils; import at.gv.egovernment.moa.id.process.api.ExecutionContext; import at.gv.egovernment.moa.id.protocols.pvp2x.utils.SAML2Utils; import at.gv.egovernment.moa.id.util.SSLUtils; @@ -62,7 +61,7 @@ public class CreateQualeIDRequestTask extends AbstractAuthServletTask { IOAAuthParameters oaConfig = pendingReq.getOnlineApplicationConfiguration(); //get basic configuration parameters - String vdaQualeIDUrl = extractVDAURLForSpecificOA(oaConfig); + String vdaQualeIDUrl = extractVDAURLForSpecificOA(oaConfig, executionContext); if (MiscUtil.isEmpty(vdaQualeIDUrl)) { Logger.error("NO VDA URL for qualified eID (" + Constants.CONFIG_PROP_VDA_ENDPOINT_QUALeID_DEFAULT + ")"); throw new SL20Exception("sl20.03", new Object[]{"NO VDA URL for qualified eID"}); @@ -83,17 +82,21 @@ public class CreateQualeIDRequestTask extends AbstractAuthServletTask { //build qualifiedeID command Map<String, String> qualifiedeIDParams = new HashMap<String, String>(); qualifiedeIDParams.put(SL20Constants.SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPUNIQUEID, oaConfig.getPublicURLPrefix()); - qualifiedeIDParams.put(SL20Constants.SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPFRIENDLYNAME, oaConfig.getFriendlyName()); + qualifiedeIDParams.put(SL20Constants.SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPFRIENDLYNAME, oaConfig.getFriendlyName()); + qualifiedeIDParams.put(SL20Constants.SL20_COMMAND_PARAM_EID_ATTRIBUTES_SPCOUNTRYCODE, "AT"); //qualifiedeIDParams.put(SL20Constants.SL20_COMMAND_PARAM_EID_ATTRIBUTES_MANDATEREFVALUE, UUID.randomUUID().toString()); + //TODO: JsonObject qualeIDCommandParams = SL20JSONBuilderUtils.createQualifiedeIDCommandParameters( authBlockId, dataURL, qualifiedeIDParams, - joseTools.getEncryptionCertificate()); + //joseTools.getEncryptionCertificate()); + null); //String qualeIDReqId = UUID.randomUUID().toString(); - String qualeIDReqId = SAML2Utils.getSecureIdentifier(); + //TODO: work-Around for A-trust + String qualeIDReqId = SAML2Utils.getSecureIdentifier().substring(0, 12); String signedQualeIDCommand = SL20JSONBuilderUtils.createSignedCommand(SL20Constants.SL20_COMMAND_IDENTIFIER_QUALIFIEDEID, qualeIDCommandParams, joseTools); JsonObject sl20Req = SL20JSONBuilderUtils.createGenericRequest(qualeIDReqId, null, null, signedQualeIDCommand); @@ -105,19 +108,21 @@ public class CreateQualeIDRequestTask extends AbstractAuthServletTask { sslFactory, authConfig.getBasicMOAIDConfigurationBoolean(AuthConfiguration.PROP_KEY_OVS_SSL_HOSTNAME_VALIDATION, true)); - //build post request + //build http POST request HttpPost httpReq = new HttpPost(new URIBuilder(vdaQualeIDUrl).build()); - httpReq.addHeader(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE, SL20Constants.HTTP_HEADER_VALUE_NATIVE); List<NameValuePair> parameters = new ArrayList<NameValuePair>();; - - //correct one - //parameters.add(new BasicNameValuePair(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM, Base64Url.encode(sl20Req.toString().getBytes()))); - - //A-Trust current version - parameters.add(new BasicNameValuePair(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM_OLD, sl20Req.toString())); + parameters.add(new BasicNameValuePair(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM, Base64Url.encode(sl20Req.toString().getBytes()))); httpReq.setEntity(new UrlEncodedFormEntity(parameters )); + //build http GET request +// URIBuilder sl20ReqUri = new URIBuilder(vdaQualeIDUrl); +// sl20ReqUri.addParameter(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM, Base64Url.encode(sl20Req.toString().getBytes())); +// HttpGet httpReq = new HttpGet(sl20ReqUri.build()); + //set native client header + httpReq.addHeader(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE, SL20Constants.HTTP_HEADER_VALUE_NATIVE); + + Logger.trace("Request VDA via SL20 with: " + Base64Url.encode(sl20Req.toString().getBytes())); //request VDA HttpResponse httpResp = httpClient.execute(httpReq); @@ -190,26 +195,40 @@ public class CreateQualeIDRequestTask extends AbstractAuthServletTask { } - private String extractVDAURLForSpecificOA(IOAAuthParameters oaConfig) { - Map<String, String> listOfVDAs = authConfig.getBasicMOAIDConfigurationWithPrefix(Constants.CONFIG_PROP_VDA_ENDPOINT_QUALeID_LIST); - Map<String, String> listOfSPs = authConfig.getBasicMOAIDConfigurationWithPrefix(Constants.CONFIG_PROP_SP_LIST); + private String extractVDAURLForSpecificOA(IOAAuthParameters oaConfig, ExecutionContext executionContext) { - for (Entry<String, String> el : listOfSPs.entrySet()) { - List<String> spEntityIds = KeyValueUtils.getListOfCSVValues(el.getValue()); - if (spEntityIds.contains(oaConfig.getPublicURLPrefix())) { - Logger.trace("Select VDA endPoint with Id: " + el.getKey()); - if (listOfVDAs.containsKey(el.getKey())) - return listOfVDAs.get(el.getKey()); - - else - Logger.info("No VDA endPoint with Id: " + el.getKey()); - - } else - Logger.trace("SP list: " + el.getKey() + " does not contain OAIdentifier: " + oaConfig.getPublicURLPrefix()); + //selection based on EntityID +// Map<String, String> listOfVDAs = authConfig.getBasicMOAIDConfigurationWithPrefix(Constants.CONFIG_PROP_VDA_ENDPOINT_QUALeID_LIST); +// Map<String, String> listOfSPs = authConfig.getBasicMOAIDConfigurationWithPrefix(Constants.CONFIG_PROP_SP_LIST); +// +// for (Entry<String, String> el : listOfSPs.entrySet()) { +// List<String> spEntityIds = KeyValueUtils.getListOfCSVValues(el.getValue()); +// if (spEntityIds.contains(oaConfig.getPublicURLPrefix())) { +// Logger.trace("Select VDA endPoint with Id: " + el.getKey()); +// if (listOfVDAs.containsKey(el.getKey())) +// return listOfVDAs.get(el.getKey()); +// +// else +// Logger.info("No VDA endPoint with Id: " + el.getKey()); +// +// } else +// Logger.trace("SP list: " + el.getKey() + " does not contain OAIdentifier: " + oaConfig.getPublicURLPrefix()); +// +// } + + //selection based on request Header + String sl20VDATypeHeader = (String) executionContext.get(SL20Constants.HTTP_HEADER_SL20_VDA_TYPE.toLowerCase()); + if (MiscUtil.isNotEmpty(sl20VDATypeHeader)) { + String vdaURL = authConfig.getBasicMOAIDConfiguration(Constants.CONFIG_PROP_VDA_ENDPOINT_QUALeID_LIST + sl20VDATypeHeader); + if (MiscUtil.isNotEmpty(vdaURL)) + return vdaURL.trim(); + else + Logger.info("Can NOT find VDA with Id: " + sl20VDATypeHeader + ". Use default VDA"); + } - Logger.debug("NO SP specific VDA endpoint found. Use default VDA"); + Logger.info("NO SP specific VDA endpoint found. Use default VDA"); return authConfig.getBasicMOAIDConfiguration(Constants.CONFIG_PROP_VDA_ENDPOINT_QUALeID_DEFAULT); } diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/ReceiveQualeIDTask.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/ReceiveQualeIDTask.java index b7fe579a3..357ecb6ec 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/ReceiveQualeIDTask.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/ReceiveQualeIDTask.java @@ -12,6 +12,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.entity.ContentType; +import org.jose4j.base64url.Base64Url; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -37,6 +38,7 @@ import at.gv.egovernment.moa.id.commons.MOAIDAuthConstants; import at.gv.egovernment.moa.id.commons.api.exceptions.MOAIDException; import at.gv.egovernment.moa.id.process.api.ExecutionContext; import at.gv.egovernment.moa.util.MiscUtil; +import at.gv.egovernment.moa.util.StreamUtils; import at.gv.egovernment.moaspss.logging.Logger; @@ -55,17 +57,30 @@ public class ReceiveQualeIDTask extends AbstractAuthServletTask { try { //get SL2.0 command or result from HTTP request Map<String, String> reqParams = getParameters(request); - String sl20Result = reqParams.get(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM); + String sl20Result = reqParams.get(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM); + if (MiscUtil.isEmpty(sl20Result)) { - Logger.info("NO SL2.0 commando or result FOUND."); - throw new SL20Exception("sl20.04", null); + + //TODO: remove + //Workaround for SIC Handy-Signature, because it sends result in InputStream + String test = StreamUtils.readStream(request.getInputStream(), "UTF-8"); + if (MiscUtil.isNotEmpty(test)) { + Logger.info("Use SIC Handy-Signature work-around!"); + sl20Result = test.substring("slcommand=".length()); + + } else { + Logger.info("NO SL2.0 commando or result FOUND."); + throw new SL20Exception("sl20.04", null); + } } - + + Logger.trace("Received SL2.0 result: " + sl20Result); + //parse SL2.0 command/result into JSON try { JsonParser jsonParser = new JsonParser(); - JsonElement sl20Req = jsonParser.parse(sl20Result); + JsonElement sl20Req = jsonParser.parse(Base64Url.decodeToUtf8String(sl20Result)); sl20ReqObj = sl20Req.getAsJsonObject(); } catch (JsonSyntaxException e) { @@ -111,16 +126,13 @@ public class ReceiveQualeIDTask extends AbstractAuthServletTask { JsonElement qualeIDResult = SL20JSONExtractorUtils.extractSL20Result( payLoad, joseTools, authConfig.getBasicMOAIDConfigurationBoolean(Constants.CONFIG_PROP_DISABLE_EID_ENCRYPTION, true)); - + //extract attributes from result - String idlB64 = SL20JSONExtractorUtils.getStringValue(qualeIDResult.getAsJsonObject(), - SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_IDL, true); - String authBlockB64 = SL20JSONExtractorUtils.getStringValue(qualeIDResult.getAsJsonObject(), - SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, true); - String ccsURL = SL20JSONExtractorUtils.getStringValue(qualeIDResult.getAsJsonObject(), - SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_CCSURL, true); - String LoA = SL20JSONExtractorUtils.getStringValue(qualeIDResult.getAsJsonObject(), - SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_LOA, true); + Map<String, String> eIDData = SL20JSONExtractorUtils.getMapOfStringElements(qualeIDResult); + String idlB64 = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_IDL); + String authBlockB64 = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK); + String ccsURL = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_CCSURL); + String LoA = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_LOA); //cache qualified eID data into pending request pendingReq.setGenericDataToSession( @@ -233,6 +245,7 @@ public class ReceiveQualeIDTask extends AbstractAuthServletTask { redirectTwoCommand, null); + //workaround for SIC VDA if (request.getHeader(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE) != null && request.getHeader(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE).equals(SL20Constants.HTTP_HEADER_VALUE_NATIVE)) { Logger.debug("Client request containts 'native client' header ... "); diff --git a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/VerifyQualifiedeIDTask.java b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/VerifyQualifiedeIDTask.java index b5c84d315..cc74bb11a 100644 --- a/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/VerifyQualifiedeIDTask.java +++ b/id/server/modules/moa-id-module-sl20_authentication/src/main/java/at/gv/egovernment/moa/id/auth/modules/sl20_auth/tasks/VerifyQualifiedeIDTask.java @@ -6,14 +6,8 @@ import java.util.Calendar; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.opensaml.Configuration; import org.opensaml.saml2.core.Assertion; -import org.opensaml.xml.XMLObject; -import org.opensaml.xml.io.Unmarshaller; -import org.opensaml.xml.io.UnmarshallerFactory; import org.springframework.stereotype.Component; -import org.w3c.dom.Element; -import org.xml.sax.SAXException; import at.gv.egovernment.moa.id.advancedlogging.TransactionIDUtils; import at.gv.egovernment.moa.id.auth.modules.AbstractAuthServletTask; @@ -28,9 +22,7 @@ import at.gv.egovernment.moa.id.commons.api.data.IVerifiyXMLSignatureResponse; 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.AssertionAttributeExtractor; -import at.gv.egovernment.moa.id.protocols.pvp2x.utils.SAML2Utils; import at.gv.egovernment.moa.util.Base64Utils; -import at.gv.egovernment.moa.util.DOMUtils; import at.gv.egovernment.moa.util.DateTimeUtils; import at.gv.egovernment.moaspss.logging.Logger; @@ -75,7 +67,7 @@ public class VerifyQualifiedeIDTask extends AbstractAuthServletTask { IIdentityLink idl = new IdentityLinkAssertionParser(new ByteArrayInputStream(Base64Utils.decode(idlB64, false))).parseIdentityLink(); IVerifiyXMLSignatureResponse authBlockVerificationResult = null; try { - Assertion authBlock = parseAuthBlockToSaml2Assertion(authBlockB64); + Assertion authBlock = QualifiedeIDVerifier.parseAuthBlockToSaml2Assertion(authBlockB64); AssertionAttributeExtractor authBlockExtractor = new AssertionAttributeExtractor(authBlock); @@ -126,55 +118,5 @@ public class VerifyQualifiedeIDTask extends AbstractAuthServletTask { TransactionIDUtils.removeSessionId(); } - } - - private Assertion parseAuthBlockToSaml2Assertion(String authblockB64) throws SL20eIDDataValidationException { - try { - //parse authBlock into SAML2 Assertion - byte[] authBlockBytes = Base64Utils.decode(authblockB64, false); - Element authBlockDOM = DOMUtils.parseXmlValidating(new ByteArrayInputStream(authBlockBytes)); - UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); - Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(authBlockDOM); - XMLObject samlAssertion = unmarshaller.unmarshall(authBlockDOM); - - //validate SAML2 Assertion - SAML2Utils.schemeValidation(samlAssertion); - - if (samlAssertion instanceof Assertion) - return (Assertion) samlAssertion; - else - throw new SL20eIDDataValidationException( - new Object[] { - SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, - "AuthBlock is NOT of type SAML2 Assertion" - }); - - } catch (SL20eIDDataValidationException e) { - throw e; - - } catch (SAXException e) { - Logger.info("Scheme validation of SAML2 AuthBlock FAILED. Reason: " + e.getMessage()); - throw new SL20eIDDataValidationException( - new Object[] { - SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, - e.getMessage() - }, - e); - - } catch (Exception e) { - Logger.info("Can not parse AuthBlock. Reason: " + e.getMessage()); - Logger.trace("FullAuthBlock: " + authblockB64); - throw new SL20eIDDataValidationException( - new Object[] { - SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, - e.getMessage() - }, - e); - - } - - } - - - + } } |