/**************************************************************************** * Copyright (C) 2012-2015 HS Coburg. * All rights reserved. * Contact: ecsec GmbH (info@ecsec.de) * * This file is part of the Open eCard App. * * GNU General Public License Usage * This file may be used under the terms of the GNU General Public * License version 3.0 as published by the Free Software Foundation * and appearing in the file LICENSE.GPL included in the packaging of * this file. Please review the following information to ensure the * GNU General Public License version 3.0 requirements will be met: * http://www.gnu.org/copyleft/gpl.html. * * Other Usage * Alternatively, this file may be used in accordance with the terms * and conditions contained in a signed written agreement between * you and ecsec GmbH. * ***************************************************************************/ package org.openecard.binding.tctoken; import iso.std.iso_iec._24727.tech.schema.ActionType; import iso.std.iso_iec._24727.tech.schema.CardApplicationConnect; import iso.std.iso_iec._24727.tech.schema.CardApplicationConnectResponse; import iso.std.iso_iec._24727.tech.schema.CardApplicationDisconnect; import iso.std.iso_iec._24727.tech.schema.CardApplicationPath; import iso.std.iso_iec._24727.tech.schema.CardApplicationPathResponse; import iso.std.iso_iec._24727.tech.schema.CardApplicationPathType; import iso.std.iso_iec._24727.tech.schema.ConnectionHandleType; import iso.std.iso_iec._24727.tech.schema.StartPAOSResponse; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import javax.annotation.Nonnull; import javax.xml.transform.TransformerException; import org.openecard.addon.AddonManager; import org.openecard.addon.AddonRegistry; import org.openecard.addon.Context; import org.openecard.addon.bind.AuxDataKeys; import org.openecard.addon.bind.BindingResultCode; import org.openecard.addon.manifest.AddonSpecification; import org.openecard.addon.manifest.ProtocolPluginSpecification; import org.openecard.binding.tctoken.ex.InvalidAddressException; import org.openecard.binding.tctoken.ex.InvalidRedirectUrlException; import org.openecard.binding.tctoken.ex.NonGuiException; import org.openecard.binding.tctoken.ex.SecurityViolationException; import org.openecard.bouncycastle.crypto.tls.Certificate; import org.openecard.common.DynamicContext; import org.openecard.common.ECardConstants; import org.openecard.common.I18n; import org.openecard.common.WSHelper; import org.openecard.common.WSHelper.WSException; import org.openecard.common.interfaces.Dispatcher; import org.openecard.common.interfaces.DispatcherException; import org.openecard.common.sal.state.CardStateEntry; import org.openecard.common.sal.state.CardStateMap; import org.openecard.common.util.Pair; import org.openecard.gui.UserConsent; import org.openecard.gui.UserConsentNavigator; import org.openecard.gui.message.DialogType; import org.openecard.recognition.CardRecognition; import org.openecard.transport.paos.PAOSException; import org.openecard.ws.marshal.WSMarshaller; import org.openecard.ws.marshal.WSMarshallerException; import org.openecard.ws.marshal.WSMarshallerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.openecard.binding.tctoken.ex.ErrorTranslations.*; import org.openecard.binding.tctoken.ex.ResultMinor; import org.openecard.common.interfaces.EventManager; import org.openecard.common.util.HandlerUtils; import org.openecard.transport.paos.PAOSConnectionException; /** * Transport binding agnostic TCToken handler. <br> * This handler supports the following transports: * <ul> * <li>PAOS</li> * </ul> * <p> * This handler supports the following security protocols: * <ul> * <li>TLS</li> * <li>TLS-PSK</li> * <li>PLS-PSK-RSA</li> * </ul> * * @author Dirk Petrautzki * @author Moritz Horsch * @author Tobias Wich * @author Hans-Martin Haase */ public class TCTokenHandler { private static final Logger logger = LoggerFactory.getLogger(TCTokenHandler.class); private static final I18n langTr03112 = I18n.getTranslation("tr03112"); private static final I18n lang = I18n.getTranslation("tctoken"); private static final I18n langPin = I18n.getTranslation("pinplugin"); private static final I18n langPace = I18n.getTranslation("pace"); // Translation constants private static final String ERROR_CARD_REMOVED = "action.error.card.removed"; private final String pin; private final String puk; private final CardStateMap cardStates; private final Dispatcher dispatcher; private final UserConsent gui; private final CardRecognition rec; private final AddonManager manager; private final EventManager evManager; /** * Creates a TCToken handler instances and initializes it with the given parameters. * * @param ctx Context containing instances to the core modules. */ public TCTokenHandler(Context ctx) { this.cardStates = ctx.getCardStates(); this.dispatcher = ctx.getDispatcher(); this.gui = ctx.getUserConsent(); this.rec = ctx.getRecognition(); this.manager = ctx.getManager(); this.evManager = ctx.getEventManager(); pin = langPace.translationForKey("pin"); puk = langPace.translationForKey("puk"); } private ConnectionHandleType prepareHandle(ConnectionHandleType connectionHandle) throws DispatcherException, InvocationTargetException, WSException { // Perform a CardApplicationPath and CardApplicationConnect to connect to the card application CardApplicationPath appPath = new CardApplicationPath(); appPath.setCardAppPathRequest(connectionHandle); CardApplicationPathResponse appPathRes = (CardApplicationPathResponse) dispatcher.deliver(appPath); // Check CardApplicationPathResponse WSHelper.checkResult(appPathRes); CardApplicationConnect appConnect = new CardApplicationConnect(); List<CardApplicationPathType> pathRes; pathRes = appPathRes.getCardAppPathResultSet().getCardApplicationPathResult(); appConnect.setCardApplicationPath(pathRes.get(0)); CardApplicationConnectResponse appConnectRes; appConnectRes = (CardApplicationConnectResponse) dispatcher.deliver(appConnect); // Update ConnectionHandle. It now includes a SlotHandle. connectionHandle = appConnectRes.getConnectionHandle(); // Check CardApplicationConnectResponse WSHelper.checkResult(appConnectRes); return connectionHandle; } /** * Performs the actual PAOS procedure. * Connects the given card, establishes the HTTP channel and talks to the server. Afterwards disconnects the card. * * @param token The TCToken containing the connection parameters. * @param connectionHandle The handle of the card that will be used. * @return A TCTokenResponse indicating success or failure. * @throws DispatcherException If there was a problem dispatching a request from the server. * @throws PAOSException If there was a transport error. */ private TCTokenResponse processBinding(TCTokenRequest tokenRequest, ConnectionHandleType connectionHandle) throws PAOSException, DispatcherException { TCToken token = tokenRequest.getTCToken(); try { connectionHandle = prepareHandle(connectionHandle); // save handle for later use DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY); dynCtx.put(TR03112Keys.CONNECTION_HANDLE, HandlerUtils.copyHandle(connectionHandle)); TCTokenResponse response = new TCTokenResponse(); response.setTCToken(token); response.setResult(WSHelper.makeResultOK()); String binding = token.getBinding(); switch (binding) { case "urn:liberty:paos:2006-08": { // send StartPAOS List<String> supportedDIDs = getSupportedDIDs(); PAOSTask task = new PAOSTask(dispatcher, connectionHandle, supportedDIDs, tokenRequest, gui); FutureTask<StartPAOSResponse> paosTask = new FutureTask<>(task); Thread paosThread = new Thread(paosTask, "PAOS"); paosThread.start(); if (! tokenRequest.isTokenFromObject()) { // wait for computation to finish waitForTask(paosTask); } response.setBindingTask(paosTask); break; } case "urn:ietf:rfc:2616":{ // no actual binding, just connect via tls and authenticate the user with that connection HttpGetTask task = new HttpGetTask(dispatcher, connectionHandle, tokenRequest); FutureTask<StartPAOSResponse> tlsTask = new FutureTask<>(task); Thread tlsThread = new Thread(tlsTask, "TLS Auth"); tlsThread.start(); waitForTask(tlsTask); response.setBindingTask(tlsTask); break; } default: // unknown binding throw new RuntimeException("Unsupported binding in TCToken."); } return response; } catch (WSException ex) { String msg = "Failed to connect to card."; logger.error(msg, ex); throw new DispatcherException(msg, ex); } catch (InvocationTargetException ex) { logger.error(ex.getMessage(), ex); throw new DispatcherException(ex); } } public static void disconnectHandle(Dispatcher dispatcher, ConnectionHandleType connectionHandle) throws DispatcherException { try { // disconnect card after authentication CardApplicationDisconnect appDis = new CardApplicationDisconnect(); appDis.setConnectionHandle(connectionHandle); appDis.setAction(ActionType.RESET); dispatcher.deliver(appDis); } catch (InvocationTargetException ex) { logger.error(ex.getMessage(), ex); throw new DispatcherException(ex); } } public static void killUserConsent() { // kill any open dialog DynamicContext ctx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY); Object navObj = ctx.get(TR03112Keys.OPEN_USER_CONSENT_NAVIGATOR); if (navObj instanceof UserConsentNavigator) { UserConsentNavigator nav = (UserConsentNavigator) navObj; nav.close(); } } /** * Activates the client according to the received TCToken. * * @param request The activation request containing the TCToken. * @return The response containing the result of the activation process. * @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined. * @throws SecurityViolationException * @throws NonGuiException */ public TCTokenResponse handleActivate(TCTokenRequest request) throws InvalidRedirectUrlException, SecurityViolationException, NonGuiException { TCToken token = request.getTCToken(); if (logger.isDebugEnabled()) { try { WSMarshaller m = WSMarshallerFactory.createInstance(); logger.debug("TCToken:\n{}", m.doc2str(m.marshal(token))); } catch (TransformerException | WSMarshallerException ex) { // it's no use } } final DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY); boolean performChecks = isPerformTR03112Checks(request); if (! performChecks) { logger.warn("Checks according to BSI TR03112 3.4.2, 3.4.4 (TCToken specific) and 3.4.5 are disabled."); } boolean isObjectActivation = request.getTCTokenURL() == null; if (isObjectActivation) { logger.warn("Checks according to BSI TR03112 3.4.4 (TCToken specific) are disabled."); } dynCtx.put(TR03112Keys.TCTOKEN_CHECKS, performChecks); dynCtx.put(TR03112Keys.OBJECT_ACTIVATION, isObjectActivation); dynCtx.put(TR03112Keys.TCTOKEN_SERVER_CERTIFICATES, request.getCertificates()); ConnectionHandleType connectionHandle = null; TCTokenResponse response = new TCTokenResponse(); response.setTCToken(token); byte[] requestedContextHandle = request.getContextHandle(); String ifdName = request.getIFDName(); BigInteger requestedSlotIndex = request.getSlotIndex(); // we know exactly which card we want ConnectionHandleType requestedHandle = new ConnectionHandleType(); requestedHandle.setContextHandle(requestedContextHandle); requestedHandle.setIFDName(ifdName); requestedHandle.setSlotIndex(requestedSlotIndex); Set<CardStateEntry> matchingHandles = cardStates.getMatchingEntries(requestedHandle); if (!matchingHandles.isEmpty()) { connectionHandle = matchingHandles.toArray(new CardStateEntry[]{})[0].handleCopy(); } if (connectionHandle == null) { String msg = lang.translationForKey("cancel"); logger.error(msg); response.setResult(WSHelper.makeResultError(ResultMinor.CANCELLATION_BY_USER, msg)); // fill in values, so it is usuable by the transport module response = determineRefreshURL(request, response); response.finishResponse(true); return response; } try { // process binding and follow redirect addresses afterwards response = processBinding(request, connectionHandle); // fill in values, so it is usuable by the transport module response = determineRefreshURL(request, response); response.finishResponse(isObjectActivation); return response; } catch (DispatcherException w) { logger.error(w.getMessage(), w); response.setResultCode(BindingResultCode.INTERNAL_ERROR); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, w.getMessage())); showErrorMessage(w.getMessage()); throw new NonGuiException(response, w.getMessage(), w); } catch (PAOSException w) { logger.error(w.getMessage(), w); // find actual error to display to the user Throwable innerException = w.getCause(); if (innerException == null) { innerException = w; } else if (innerException instanceof ExecutionException) { innerException = innerException.getCause(); } String errorMsg = innerException.getLocalizedMessage(); switch (errorMsg) { case "The target server failed to respond": errorMsg = langTr03112.translationForKey(NO_RESPONSE_FROM_SERVER); break; case ECardConstants.Minor.App.INT_ERROR + " ==> Unknown eCard exception occurred.": errorMsg = langTr03112.translationForKey(UNKNOWN_ECARD_ERROR); break; case "Internal TLS error, this could be an attack": errorMsg = langTr03112.translationForKey(INTERNAL_TLS_ERROR); break; } if (innerException instanceof WSException) { WSException ex = (WSException) innerException; errorMsg = createResponseFromWsEx(ex, response); } else if (innerException instanceof PAOSConnectionException) { response.setResult(WSHelper.makeResultError(ResultMinor.TRUSTED_CHANNEL_ESTABLISCHMENT_FAILED, w.getLocalizedMessage())); } else { errorMsg = createMessageFromUnknownError(w); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, w.getMessage())); } showErrorMessage(errorMsg); try { // fill in values, so it is usuable by the transport module response = determineRefreshURL(request, response); response.finishResponse(true); } catch (InvalidRedirectUrlException ex) { logger.error(ex.getMessage(), ex); response.setResultCode(BindingResultCode.INTERNAL_ERROR); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, ex.getLocalizedMessage())); throw new NonGuiException(response, ex.getMessage(), ex); } catch (SecurityViolationException ex) { String msg2 = "The RefreshAddress contained in the TCToken is invalid. Redirecting to the " + "CommunicationErrorAddress."; logger.error(msg2, ex); response.setResultCode(BindingResultCode.REDIRECT); response.setResult(WSHelper.makeResultError(ResultMinor.COMMUNICATION_ERROR, msg2)); response.addAuxResultData(AuxDataKeys.REDIRECT_LOCATION, ex.getBindingResult().getAuxResultData().get( AuxDataKeys.REDIRECT_LOCATION)); } return response; } } private static void waitForTask(Future<?> task) throws PAOSException, DispatcherException { try { task.get(); } catch (InterruptedException ex) { logger.error(ex.getMessage(), ex); throw new PAOSException(ex); } catch (ExecutionException ex) { logger.error(ex.getMessage(), ex); // perform conversion of ExecutionException from the Future to the really expected exceptions if (ex.getCause() instanceof PAOSException) { throw (PAOSException) ex.getCause(); } else if (ex.getCause() instanceof DispatcherException) { throw (DispatcherException) ex.getCause(); } else { throw new PAOSException(ex); } } } /** * Follow the URL in the RefreshAddress and update it in the response. * The redirect is followed as long as the response is a redirect (302, 303 or 307) AND is a * https-URL AND the hash of the retrieved server certificate is contained in the CertificateDescrioption, else * return 400. If the URL and the subjectURL in the CertificateDescription conform to the SOP we reached our final * destination. * * @param request TCToken request used to determine which security checks to perform. * @param response The TCToken response in which the original refresh address is defined and where it will be * updated. * @return Modified response with the final address the browser should be redirected to. * @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined. */ private static TCTokenResponse determineRefreshURL(TCTokenRequest request, TCTokenResponse response) throws InvalidRedirectUrlException, SecurityViolationException { try { String endpointStr = response.getRefreshAddress(); URL endpoint = new URL(endpointStr); DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY); // omit checks completely if this is an object tag activation Object objectActivation = dynCtx.get(TR03112Keys.OBJECT_ACTIVATION); if (objectActivation instanceof Boolean && ((Boolean) objectActivation) == true) { return response; } // disable certificate checks according to BSI TR03112-7 in some situations boolean redirectChecks = isPerformTR03112Checks(request); RedirectCertificateValidator verifier = new RedirectCertificateValidator(redirectChecks); ResourceContext ctx = ResourceContext.getStream(endpoint, verifier); ctx.closeStream(); // using this verifier no result must be present, meaning no status code different than a redirect occurred // if (result.p1 != null) { // // TODO: this error is expected according the spec, handle it in a different way // String msg = "Return-To-Websession yielded a non-redirect response."; // throw new IOException(msg); // } // determine redirect List<Pair<URL, Certificate>> resultPoints = ctx.getCerts(); Pair<URL, Certificate> last = resultPoints.get(resultPoints.size() - 1); endpoint = last.p1; dynCtx.put(TR03112Keys.IS_REFRESH_URL_VALID, true); logger.debug("Setting redirect address to '{}'.", endpoint); response.setRefreshAddress(endpoint.toString()); return response; } catch (MalformedURLException ex) { throw new IllegalStateException(langTr03112.translationForKey(REFRESH_URL_ERROR), ex); } catch (ResourceException | InvalidAddressException | ValidationError | IOException ex) { String code = ECardConstants.Minor.App.COMMUNICATION_ERROR; String communicationErrorAddress = response.getTCToken().getComErrorAddressWithParams(code); if (communicationErrorAddress != null && ! communicationErrorAddress.isEmpty()) { throw new SecurityViolationException(communicationErrorAddress, REFRESH_DETERMINATION_FAILED, ex); } throw new InvalidRedirectUrlException(REFRESH_DETERMINATION_FAILED, ex); } } private List<String> getSupportedDIDs() { TreeSet<String> result = new TreeSet<>(); // check all sal protocols in the AddonRegistry registry = manager.getRegistry(); Set<AddonSpecification> addons = registry.listAddons(); for (AddonSpecification addon : addons) { for (ProtocolPluginSpecification proto : addon.getSalActions()) { result.add(proto.getUri()); } } return new ArrayList<>(result); } /** * Checks if checks according to BSI TR03112-7 3.4.2, 3.4.4 and 3.4.5 must be performed. * * @param tcTokenRequest TC Token request. * @return {@code true} if checks should be performed, {@code false} otherwise. */ private static boolean isPerformTR03112Checks(TCTokenRequest tcTokenRequest) { boolean activationChecks = true; // disable checks when not using the nPA if (! tcTokenRequest.getCardType().equals("http://bsi.bund.de/cif/npa.xml")) { activationChecks = false; } return activationChecks; } private void showBackgroundMessage(final String msg, final String title, final DialogType dialogType) { new Thread(new Runnable() { @Override public void run() { gui.obtainMessageDialog().showMessageDialog(msg, title, dialogType); } }, "Background_MsgBox").start(); } private void showErrorMessage(String errMsg) { String title = langTr03112.translationForKey(ERROR_TITLE); String baseHeader = langTr03112.translationForKey(ERROR_HEADER); String exceptionPart = langTr03112.translationForKey(ERROR_MSG_IND); String removeCard = langTr03112.translationForKey(REMOVE_CARD); String msg = String.format("%s\n\n%s\n%s\n\n%s", baseHeader, exceptionPart, errMsg, removeCard); showBackgroundMessage(msg, title, DialogType.ERROR_MESSAGE); } private String createResponseFromWsEx(WSException ex, TCTokenResponse response) { String errorMsg; switch (ex.getResultMinor()) { case ECardConstants.Minor.SAL.CANCELLATION_BY_USER: errorMsg = lang.translationForKey("cancel"); response.setResult(WSHelper.makeResultError(ResultMinor.CANCELLATION_BY_USER, errorMsg)); break; case ECardConstants.Minor.SAL.EAC.DOC_VALID_FAILED: errorMsg = langTr03112.translationForKey(CERT_ERROR); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, errorMsg)); break; case ECardConstants.Minor.App.INCORRECT_PARM: errorMsg = langTr03112.translationForKey(MESSAGE_CONTENT_INVALID); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, errorMsg)); break; case ECardConstants.Minor.App.INT_ERROR: errorMsg = langTr03112.translationForKey(INTERNAL_ERROR); response.setResult(WSHelper.makeResultError(ResultMinor.SERVER_ERROR, errorMsg)); break; case ECardConstants.Minor.SAL.PREREQUISITES_NOT_SATISFIED: errorMsg = langTr03112.translationForKey(CERT_DESCRIPTION_CHECK_FAILED); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, errorMsg)); break; case ECardConstants.Minor.App.UNKNOWN_ERROR: errorMsg = langTr03112.translationForKey(ERROR_WHILE_AUTHENTICATION); response.setResult(WSHelper.makeResultError(ResultMinor.SERVER_ERROR, errorMsg)); break; case ECardConstants.Minor.SAL.UNKNOWN_HANDLE: errorMsg = langTr03112.translationForKey(UNKNOWN_CONNECTION_HANDLE); response.setResult(WSHelper.makeResultError(ResultMinor.SERVER_ERROR, errorMsg)); break; case ECardConstants.Minor.IFD.INVALID_SLOT_HANDLE: errorMsg = langPin.translationForKey(ERROR_CARD_REMOVED); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, errorMsg)); break; case ECardConstants.Minor.IFD.PASSWORD_BLOCKED: errorMsg = langPace.translationForKey("step_error_pin_blocked", pin, pin, puk, pin); response.setResult(WSHelper.makeResultError(ResultMinor.CLIENT_ERROR, errorMsg)); break; default: errorMsg = langTr03112.translationForKey(ERROR_WHILE_AUTHENTICATION); response.setResult(WSHelper.makeResultError(ResultMinor.SERVER_ERROR, errorMsg)); } return errorMsg; } /** * Creates an error message from an PAOSException which contains a not handled inner exception. * * @param w An PAOSException containing a not handled inner exception. * @return A sting containing an error message. */ private String createMessageFromUnknownError(@Nonnull PAOSException w) { String errorMsg = "\n"; errorMsg += langTr03112.translationForKey(UNHANDLED_INNER_EXCEPTION); errorMsg += "\n"; errorMsg += w.getMessage(); return errorMsg; } }