/* * Copyright 2018 A-SIT Plus GmbH * AT-specific eIDAS Connector has been developed in a cooperation between EGIZ, * A-SIT Plus GmbH, A-SIT, and Graz University of Technology. * * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "License"); * You may not use this work except in compliance with the License. * You may obtain a copy of the License at: * https://joinup.ec.europa.eu/news/understanding-eupl-v12 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * 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.asitplus.eidas.specific.modules.auth.eidas.v2.clients.szr; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.xml.XMLConstants; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; import javax.xml.namespace.QName; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.ws.BindingProvider; import javax.xml.ws.Dispatch; import org.apache.commons.lang3.StringUtils; import org.apache.xpath.XPathAPI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants; import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.AbstractSoapClient; import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.AbstractSoapClient.HttpClientConfig.HttpClientConfigBuilder; import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.MatchedPersonResult; import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.SzrCommunicationException; import at.gv.e_government.reference.namespace.persondata._20020228.AlternativeNameType; import at.gv.e_government.reference.namespace.persondata._20020228.IdentificationType; import at.gv.e_government.reference.namespace.persondata._20020228.PersonNameType; import at.gv.e_government.reference.namespace.persondata._20020228.PhysicalPersonType; import at.gv.egiz.eaaf.core.api.data.EaafConstants; import at.gv.egiz.eaaf.core.api.data.PvpAttributeDefinitions; import at.gv.egiz.eaaf.core.api.data.XmlNamespaceConstants; import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; import at.gv.egiz.eaaf.core.impl.utils.DomUtils; import szrservices.GetBPK; import szrservices.GetBPKResponse; import szrservices.GetIdentityLinkEidas; import szrservices.GetIdentityLinkEidasResponse; import szrservices.IdentityLinkType; import szrservices.JwsHeaderParam; import szrservices.ObjectFactory; import szrservices.PersonInfoType; import szrservices.SZR; import szrservices.SZRException_Exception; import szrservices.SignContent; import szrservices.SignContentEntry; import szrservices.SignContentResponseType; import szrservices.TravelDocumentType; @Service("SZRClientForeIDAS") public class SzrClient extends AbstractSoapClient { private static final Logger log = LoggerFactory.getLogger(SzrClient.class); private static final String CLIENT_DEFAULT = "DefaultClient"; private static final String CLIENT_RAW = "RawClient"; private static final String ATTR_NAME_VSZ = "urn:eidgvat:attributes.vsz.value"; private static final String ATTR_NAME_PUBKEYS = "urn:eidgvat:attributes.user.pubkeys"; private static final String ATTR_NAME_STATUS = "urn:eidgvat:attributes.eid.status"; private static final String KEY_BC_BIND = "bcBindReq"; private static final String JOSE_HEADER_USERCERTPINNING_TYPE = "urn:at.gv.eid:bindtype"; private static final String JOSE_HEADER_USERCERTPINNING_EIDASBIND = "urn:at.gv.eid:eidasBind"; public static final String ATTR_NAME_MDS = "urn:eidgvat:mds"; // client for anything, without identitylink private SZR szr = null; // RAW client is needed for identitylink private Dispatch dispatch = null; final ObjectMapper mapper = new ObjectMapper(); /** * Get IdentityLink of a person. * * * @param eidData minimum dataset of person * @return IdentityLink * @throws SzrCommunicationException In case of a SZR error */ public IdentityLinkType getIdentityLinkInRawMode(SimpleEidasData eidData) throws SzrCommunicationException { try { final GetIdentityLinkEidas getIdl = new GetIdentityLinkEidas(); getIdl.setPersonInfo(generateSzrRequest(eidData)); return getIdentityLinkGeneric(getIdl); } catch (final Exception e) { log.warn("SZR communication FAILED. Reason: " + e.getMessage(), e); throw new SzrCommunicationException("ernb.02", new Object[]{e.getMessage()}, e); } } /** * Get IdentityLink of a person. * * * @param matchedPersonData eID information of an already matched person. * @return IdentityLink * @throws SzrCommunicationException In case of a SZR error */ public IdentityLinkType getIdentityLinkInRawMode(MatchedPersonResult matchedPersonData) throws SzrCommunicationException { try { final GetIdentityLinkEidas getIdl = new GetIdentityLinkEidas(); getIdl.setPersonInfo(generateSzrRequest(matchedPersonData)); return getIdentityLinkGeneric(getIdl); } catch (final Exception e) { log.warn("SZR communication FAILED. Reason: " + e.getMessage(), e); throw new SzrCommunicationException("ernb.02", new Object[]{e.getMessage()}, e); } } /** * Get bPK of person. * * * @param eidData Minimum dataset of person * @param target requested bPK target * @param vkz Verfahrenskennzeichen * @return bPK for this person * @throws SzrCommunicationException In case of a SZR error */ public List getBpk(SimpleEidasData eidData, String target, String vkz) throws SzrCommunicationException { try { final GetBPK parameters = new GetBPK(); parameters.setPersonInfo(generateSzrRequest(eidData)); parameters.getBereichsKennung().add(target); parameters.setVKZ(vkz); final GetBPKResponse result = this.szr.getBPK(parameters); return result.getGetBPKReturn(); } catch (final SZRException_Exception e) { log.warn("SZR communication FAILED. Reason: " + e.getMessage(), e); throw new SzrCommunicationException("ernb.02", new Object[]{e.getMessage()}, e); } } /** * Creates a new ERnP entry. * TODO Is this correct? Ask BMI. * * @param eidasData Minimum dataset of person * @return encrypted baseId * @throws SzrCommunicationException In case of a SZR error */ public String createNewErnpEntry(final SimpleEidasData eidasData) throws SzrCommunicationException { final String resp; try { resp = this.szr.getStammzahlEncrypted(generateSzrRequest(eidasData), true); } catch (SZRException_Exception e) { throw new SzrCommunicationException("ernb.02", new Object[]{e.getMessage()}, e); } if (StringUtils.isEmpty(resp)) { throw new SzrCommunicationException("ernb.01", new Object[]{"Stammzahl response empty"}); // TODO error handling } return resp; } /** * Request a encrypted baseId from SZR. * * Note: Previously, this method did create a new ERnP entry, if it did not exist. This is * not the case any more. See {@link #createNewErnpEntry(SimpleEidasData)} for that functionality. * * @param eidData Minimum dataset of person * @return encrypted baseId * @throws SzrCommunicationException In case of a SZR error */ public String getEncryptedStammzahl(final SimpleEidasData eidData) throws SzrCommunicationException { final String resp; try { resp = this.szr.getStammzahlEncrypted(generateSzrRequest(eidData), false); } catch (SZRException_Exception e) { throw new SzrCommunicationException("ernb.02", new Object[]{e.getMessage()}, e); } if (StringUtils.isEmpty(resp)) { throw new SzrCommunicationException("ernb.01", new Object[]{"Stammzahl response empty"}); // TODO error handling } return resp; } /** * Request a encrypted baseId from SZR. * * @param matchedPersonData eID information of an already matched person. * @return encrypted baseId * @throws SzrCommunicationException In case of a SZR error */ public String getEncryptedStammzahl(MatchedPersonResult matchedPersonData) throws SzrCommunicationException { final String resp; try { resp = this.szr.getStammzahlEncrypted(generateSzrRequest(matchedPersonData), false); } catch (SZRException_Exception e) { throw new SzrCommunicationException("ernb.02", new Object[]{e.getMessage()}, e); } if (StringUtils.isEmpty(resp)) { throw new SzrCommunicationException("ernb.01", new Object[]{"Stammzahl response empty"}); // TODO error handling } return resp; } /** * Sign an eidasBind data-structure that combines vsz with user's pubKey and E-ID status. * * @param vsz encrypted baseId * @param bindingPubKey binding PublicKey as PKCS1# (ASN.1) container * @param eidStatus Status of the E-ID * @param eidData eID information that was used for ERnP registration * @return bPK for this person * @throws SzrCommunicationException In case of a SZR error */ public String getEidasBind(final String vsz, final String bindingPubKey, final String eidStatus, SimpleEidasData eidData)throws SzrCommunicationException { final Map eidsaBindMap = new HashMap<>(); eidsaBindMap.put(ATTR_NAME_VSZ, vsz); eidsaBindMap.put(ATTR_NAME_STATUS, eidStatus); eidsaBindMap.put(ATTR_NAME_PUBKEYS, Collections.singletonList(bindingPubKey)); eidsaBindMap.put(PvpAttributeDefinitions.EID_ISSUING_NATION_NAME, eidData.getCitizenCountryCode()); injectMdsIfAvailableAndActive(eidsaBindMap, eidData); try { final String serializedEidasBind = mapper.writeValueAsString(eidsaBindMap); final SignContent req = new SignContent(); final SignContentEntry eidasBindInfo = new SignContentEntry(); eidasBindInfo.setKey(KEY_BC_BIND); eidasBindInfo.setValue(serializedEidasBind); req.getIn().add(eidasBindInfo); req.setAppendCert(false); final JwsHeaderParam eidasBindJoseHeader = new JwsHeaderParam(); eidasBindJoseHeader.setKey(JOSE_HEADER_USERCERTPINNING_TYPE); eidasBindJoseHeader.setValue(JOSE_HEADER_USERCERTPINNING_EIDASBIND); req.getJWSHeaderParam().add(eidasBindJoseHeader); log.trace("Requesting SZR to sign bcBind datastructure ... "); final SignContentResponseType resp = szr.signContent(req.isAppendCert(), req.getJWSHeaderParam(), req.getIn()); log.trace("Receive SZR response on bcBind siging operation "); if (resp == null || resp.getOut() == null || resp.getOut().isEmpty() || StringUtils.isEmpty(resp.getOut().get(0).getValue())) { throw new SzrCommunicationException("ernb.01", new Object[]{"BcBind response empty"}); } return resp.getOut().get(0).getValue(); } catch (final JsonProcessingException | SZRException_Exception e) { log.warn("Requesting bcBind by using SZR FAILED.", e); throw new SzrCommunicationException("ernb.02", new Object[]{e.getMessage()}, e); } } private PersonInfoType generateSzrRequest(MatchedPersonResult matchedPersonData) { log.trace("Starting connecting SZR Gateway"); final PersonInfoType personInfo = new PersonInfoType(); final PersonNameType personName = new PersonNameType(); final PhysicalPersonType naturalPerson = new PhysicalPersonType(); IdentificationType bpk = new IdentificationType(); naturalPerson.setName(personName); personInfo.setPerson(naturalPerson); naturalPerson.setIdentification(bpk); // person information personName.setFamilyName(matchedPersonData.getFamilyName()); personName.setGivenName(matchedPersonData.getGivenName()); naturalPerson.setDateOfBirth(matchedPersonData.getDateOfBirth()); bpk.setValue(matchedPersonData.getBpk()); bpk.setType(EaafConstants.URN_PREFIX_CDID + "ZP"); return personInfo; } private PersonInfoType generateSzrRequest(SimpleEidasData eidData) { log.trace("Starting connecting SZR Gateway"); final PersonInfoType personInfo = new PersonInfoType(); final PersonNameType personName = new PersonNameType(); final PhysicalPersonType naturalPerson = new PhysicalPersonType(); final TravelDocumentType eDocument = new TravelDocumentType(); naturalPerson.setName(personName); personInfo.setPerson(naturalPerson); personInfo.setTravelDocument(eDocument); // person information personName.setFamilyName(eidData.getFamilyName()); personName.setGivenName(eidData.getGivenName()); naturalPerson.setDateOfBirth(eidData.getDateOfBirth()); //TODO: need to be updated to new eIDAS document interface!!!! eDocument.setIssuingCountry(eidData.getCitizenCountryCode()); eDocument.setDocumentNumber(eidData.getPseudonym()); // eID document information String documentType = basicConfig .getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_PARAMS_EDOCUMENTTYPE, Constants.SZR_CONSTANTS_DEFAULT_DOCUMENT_TYPE); eDocument.setDocumentType(documentType); // set PlaceOfBirth if available if (eidData.getPlaceOfBirth() != null) { log.trace("Find 'PlaceOfBirth' attribute: " + eidData.getPlaceOfBirth()); boolean setPlaceOfBirth = basicConfig .getBasicConfigurationBoolean(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_PARAMS_SETPLACEOFBIRTHIFAVAILABLE, true); if (setPlaceOfBirth) { naturalPerson.setPlaceOfBirth(eidData.getPlaceOfBirth()); log.trace("Adding 'PlaceOfBirth' to ERnB request ... "); } } // set BirthName if available if (eidData.getBirthName() != null) { log.trace("Find 'BirthName' attribute: " + eidData.getBirthName()); boolean setBirthName = basicConfig .getBasicConfigurationBoolean(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_PARAMS_SETBIRTHNAMEIFAVAILABLE, true); if (setBirthName) { final AlternativeNameType alternativeName = new AlternativeNameType(); naturalPerson.setAlternativeName(alternativeName); alternativeName.setFamilyName(eidData.getBirthName()); log.trace("Adding 'BirthName' to ERnB request ... "); } } return personInfo; } private IdentityLinkType getIdentityLinkGeneric(GetIdentityLinkEidas getIdl) throws Exception { final JAXBContext jaxbContext = JAXBContext.newInstance(ObjectFactory.class); final Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); jaxbMarshaller.marshal(getIdl, outputStream); outputStream.flush(); final Source source = new StreamSource(new ByteArrayInputStream(outputStream.toByteArray())); outputStream.close(); log.trace("Requesting SZR ... "); final Source response = dispatch.invoke(source); log.trace("Receive RAW response from SZR"); final byte[] szrResponse = sourceToByteArray(response); final GetIdentityLinkEidasResponse jaxbElement = (GetIdentityLinkEidasResponse) jaxbContext .createUnmarshaller().unmarshal(new ByteArrayInputStream(szrResponse)); // build response log.trace(new String(szrResponse, StandardCharsets.UTF_8)); // ok, we have success final Document doc = DomUtils.parseDocument( new ByteArrayInputStream(szrResponse), true, XmlNamespaceConstants.ALL_SCHEMA_LOCATIONS + " " + Constants.SZR_SCHEMA_LOCATIONS, null, null); final String xpathExpression = "//saml:Assertion"; final Element nsNode = doc.createElementNS("urn:oasis:names:tc:SAML:1.0:assertion", "saml:NSNode"); log.trace("Selecting signed doc " + xpathExpression); final Element documentNode = (Element) XPathAPI.selectSingleNode(doc, xpathExpression, nsNode); log.trace("Signed document: " + DomUtils.serializeNode(documentNode)); final IdentityLinkType idl = new IdentityLinkType(); idl.setAssertion(documentNode); idl.setPersonInfo(jaxbElement.getGetIdentityLinkReturn().getPersonInfo()); return idl; } @PostConstruct private void initialize() throws EaafConfigurationException { log.info("Starting SZR-Client initialization .... "); final URL url = SzrClient.class.getResource("/wsdl/szr_client/SZR_v4.0.wsdl"); final boolean useTestSzr = basicConfig.getBasicConfigurationBoolean( Constants.CONIG_PROPS_EIDAS_SZRCLIENT_USETESTSERVICE, true); SzrService szrService; QName qname; String szrUrl; if (useTestSzr) { log.debug("Initializing SZR test environment configuration."); qname = SzrService.SZRTestumgebung; szrService = new SzrService(url, new QName("urn:SZRServices", "SZRService")); szr = szrService.getSzrTestumgebung(); szrUrl = basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_ENDPOINT_TEST); } else { log.debug("Initializing SZR productive configuration."); qname = SzrService.SZRProduktionsumgebung; szrService = new SzrService(url, new QName("urn:SZRServices", "SZRService")); szr = szrService.getSzrProduktionsumgebung(); szrUrl = basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_ENDPOINT_PROD); } // create raw client; dispatch = szrService.createDispatch(qname, Source.class, javax.xml.ws.Service.Mode.PAYLOAD); if (StringUtils.isEmpty(szrUrl)) { log.error("No SZR service-URL found. SZR-Client initalisiation failed."); throw new RuntimeException("No SZR service URL found. SZR-Client initalisiation failed."); } // check if Clients can be initialized if (szr == null) { log.error("SZR " + CLIENT_DEFAULT + " is 'NULL'. Something goes wrong"); throw new RuntimeException("SZR " + CLIENT_DEFAULT + " is 'NULL'. Something goes wrong"); } if (dispatch == null) { log.error("SZR " + CLIENT_RAW + " is 'NULL'. Something goes wrong"); throw new RuntimeException("SZR " + CLIENT_RAW + " is 'NULL'. Something goes wrong"); } // inject handler log.info("Use SZR service-URL: " + szrUrl); injectBindingProvider((BindingProvider) szr, CLIENT_DEFAULT, szrUrl, basicConfig.getBasicConfigurationBoolean(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_DEBUG_TRACEMESSAGES, false)); injectBindingProvider(dispatch, CLIENT_RAW, szrUrl, basicConfig.getBasicConfigurationBoolean(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_DEBUG_TRACEMESSAGES, false)); // inject http parameters and SSL context log.debug("Inject HTTP client settings ... "); HttpClientConfigBuilder httpClientBuilder = HttpClientConfig.builder() .clientName("SZR Client") .clientUrl(szrUrl) .connectionTimeout(basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_SZRCLIENT_TIMEOUT_CONNECTION, Constants.HTTP_CLIENT_DEFAULT_TIMEOUT_CONNECTION)) .responseTimeout(basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_SZRCLIENT_TIMEOUT_RESPONSE, Constants.HTTP_CLIENT_DEFAULT_TIMEOUT_RESPONSE)) .keyStoreConfig(buildKeyStoreConfiguration( Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_KEYSTORE_TYPE, Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_KEYSTORE_PATH, Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_KEYSTORE_PASSWORD, Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_KEYSTORE_NAME, "SZR SSL Client-Authentication KeyStore")) .keyAlias(basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_KEYS_ALIAS)) .keyPassword(basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_KEY_PASSWORD)) .trustAll(false) .trustStoreConfig(buildKeyStoreConfiguration( Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_TRUSTSTORE_TYPE, Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_TRUSTSTORE_PATH, Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_TRUSTSTORE_PASSWORD, Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SSL_TRUSTSTORE_NAME, "SZR SSL Client-Authentication KeyStore")); injectHttpClient(szr, httpClientBuilder.clientType(CLIENT_DEFAULT).build()); injectHttpClient(dispatch, httpClientBuilder.clientType(CLIENT_RAW).build()); log.info("SZR-Client initialization successfull"); } private void injectMdsIfAvailableAndActive(Map eidsaBindMap, SimpleEidasData eidData) { if (basicConfig.getBasicConfigurationBoolean( Constants.CONIG_PROPS_EIDAS_SZRCLIENT_SET_MDS_TO_EIDASBIND, false)) { log.info("Injecting MDS into eidasBind ... "); final Map mds = new HashMap<>(); mds.put(PvpAttributeDefinitions.PRINCIPAL_NAME_NAME, eidData.getFamilyName()); mds.put(PvpAttributeDefinitions.GIVEN_NAME_NAME, eidData.getGivenName()); mds.put(PvpAttributeDefinitions.BIRTHDATE_NAME, eidData.getDateOfBirth()); eidsaBindMap.put(ATTR_NAME_MDS, mds); } } private byte[] sourceToByteArray(Source result) throws TransformerException { final TransformerFactory factory = TransformerFactory.newInstance(); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); final Transformer transformer = factory.newTransformer(); transformer.setOutputProperty("omit-xml-declaration", "yes"); transformer.setOutputProperty("method", "xml"); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final StreamResult streamResult = new StreamResult(); streamResult.setOutputStream(out); transformer.transform(result, streamResult); return out.toByteArray(); } }