/****************************************************************************
* Copyright (C) 2012 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.control.module.tctoken;
import generated.TCTokenType;
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.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import org.openecard.apache.http.HttpException;
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.HttpRequestLineUtils;
import org.openecard.common.util.Pair;
import org.openecard.common.sal.util.InsertCardDialog;
import org.openecard.gui.UserConsent;
import org.openecard.recognition.CardRecognition;
import org.openecard.transport.paos.PAOSException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 <petrautzki@hs-coburg.de>
* @author Moritz Horsch <horsch@cdc.informatik.tu-darmstadt.de>
*/
public class TCTokenHandler {
private static final Logger logger = LoggerFactory.getLogger(TCTokenHandler.class);
private final I18n lang = I18n.getTranslation("tctoken");
private final CardStateMap cardStates;
private final Dispatcher dispatcher;
private final UserConsent gui;
private final CardRecognition rec;
/**
* Creates a TCToken handler instances and initializes it with the given parameters.
*
* @param cardStates Instance of the card states managed by the application.
* @param dispatcher The dispatcher used to deliver the messages to the webservice interface implementations.
* @param gui The implementation of the user consent interface.
* @param rec The card recognition engine.
*/
public TCTokenHandler(CardStateMap cardStates, Dispatcher dispatcher, UserConsent gui, CardRecognition rec) {
this.cardStates = cardStates;
this.dispatcher = dispatcher;
this.gui = gui;
this.rec = rec;
}
/**
* Processes the activation request sent via the localhost binding.
*
* @param requestURI The request URI of the localhost server. This is not the tcTokenURL but the complete request
* URI.
* @return A TCToken request for further processing in the TCToken handler.
* @throws UnsupportedEncodingException If the URI contains an invalid query string.
* @throws TCTokenException If the TCToken could not be fetched. That means either the URL is invalid, the server
* was not reachable or the returned value was not a TCToken or TCToken like structure.
*/
public TCTokenRequest parseRequestURI(URI requestURI) throws UnsupportedEncodingException, TCTokenException {
String queryStr = requestURI.getRawQuery();
Map<String, String> queries = HttpRequestLineUtils.transform(queryStr);
TCTokenRequest result;
if (queries.containsKey("tcTokenURL")) {
result = parseTCTokenRequestURI(queries);
result.setTokenFromObject(false);
return result;
} else if (queries.containsKey("activationObject")) {
result = parseObjectURI(queries);
result.setTokenFromObject(true);
return result;
}
throw new TCTokenException("No suitable set of parameters given in the request.");
}
public TCTokenRequest parseTCTokenRequestURI(Map<String, String> queries) throws TCTokenException {
TCTokenRequest tcTokenRequest = new TCTokenRequest();
for (Map.Entry<String, String> next : queries.entrySet()) {
String k = next.getKey();
String v = next.getValue();
if (k.equals("tcTokenURL")) {
if (v != null && ! v.isEmpty()) {
try {
URL tcTokenURL = new URL(v);
Pair<TCTokenType, List<Pair<URL, Certificate>>> token = TCTokenFactory.generateTCToken(tcTokenURL);
tcTokenRequest.setTCToken(token.p1);
tcTokenRequest.setCertificates(token.p2);
tcTokenRequest.setTCTokenURL(tcTokenURL);
} catch (MalformedURLException ex) {
String msg = "The tcTokenURL parameter contains an invalid URL: " + v;
throw new TCTokenException(msg, ex);
} catch (IOException ex) {
throw new TCTokenException("Failed to fetch TCToken.", ex);
}
} else {
throw new TCTokenException("Parameter tcTokenURL contains no value.");
}
} else if (k.equals("ifdName")) {
if (v != null && ! v.isEmpty()) {
tcTokenRequest.setIFDName(v);
} else {
throw new TCTokenException("Parameter ifdName contains no value.");
}
} else if (k.equals("contextHandle")) {
if (v != null && ! v.isEmpty()) {
tcTokenRequest.setContextHandle(v);
} else {
throw new TCTokenException("Parameter contextHandle contains no value.");
}
} else if (k.equals("slotIndex")) {
if (v != null && ! v.isEmpty()) {
tcTokenRequest.setSlotIndex(v);
} else {
throw new TCTokenException("Parameter slotIndex contains no value.");
}
} else if (k.equals("cardType")) {
if (v != null && ! v.isEmpty()) {
tcTokenRequest.setCardType(v);
} else {
throw new TCTokenException("Parameter cardType contains no value.");
}
} else {
logger.info("Unknown query element: {}", k);
}
}
return tcTokenRequest;
}
public TCTokenRequest parseObjectURI(Map<String, String> queries) throws TCTokenException {
TCTokenRequest tcTokenRequest = new TCTokenRequest();
for (Map.Entry<String, String> next : queries.entrySet()) {
String k = next.getKey();
String v = next.getValue();
if ("activationObject".equals(k)) {
TCTokenType token = TCTokenFactory.generateTCToken(v);
tcTokenRequest.setTCToken(token);
} else if ("serverCertificate".equals(k)) {
// TODO: convert base64 and url encoded certificate to Certificate object
}
}
return tcTokenRequest;
}
/**
* Gets the first handle of the given card type.
*
* @param type The card type to get the first handle for.
* @return Handle describing the given card type or null if none is present.
*/
private ConnectionHandleType getFirstHandle(String type) {
String cardName = rec.getTranslatedCardName(type);
ConnectionHandleType conHandle = new ConnectionHandleType();
ConnectionHandleType.RecognitionInfo recInfo = new ConnectionHandleType.RecognitionInfo();
recInfo.setCardType(type);
conHandle.setRecognitionInfo(recInfo);
Set<CardStateEntry> entries = cardStates.getMatchingEntries(conHandle);
if (entries.isEmpty()) {
InsertCardDialog uc = new InsertCardDialog(gui, cardStates, type, cardName);
return uc.show();
} else {
return entries.iterator().next().handleCopy();
}
}
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 {
TCTokenType token = tokenRequest.getTCToken();
try {
connectionHandle = prepareHandle(connectionHandle);
TCTokenResponse response = new TCTokenResponse();
response.setRefreshAddress(new URL(token.getRefreshAddress()));
response.setResult(WSHelper.makeResultOK());
String binding = token.getBinding();
if ("urn:liberty:paos:2006-08".equals(binding)) {
// send StartPAOS
PAOSTask task = new PAOSTask(dispatcher, connectionHandle, tokenRequest);
FutureTask<StartPAOSResponse> paosTask = new FutureTask<StartPAOSResponse>(task);
Thread paosThread = new Thread(paosTask, "PAOS");
paosThread.start();
if (! tokenRequest.isTokenFromObject()) {
// wait for computation to finish
waitForTask(paosTask);
}
response.setBindingTask(paosTask);
} else if ("urn:ietf:rfc:2616".equals(binding)) {
// 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<StartPAOSResponse>(task);
Thread tlsThread = new Thread(tlsTask, "TLS Auth");
tlsThread.start();
waitForTask(tlsTask);
response.setBindingTask(tlsTask);
} else {
// 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);
} catch (MalformedURLException ex) {
logger.error(ex.getMessage(), ex);
throw new PAOSException(ex);
}
}
public static void disconnectHandle(Dispatcher dispatcher, ConnectionHandleType connectionHandle)
throws DispatcherException {
try {
// disconnect card after authentication
CardApplicationDisconnect appDis = new CardApplicationDisconnect();
appDis.setConnectionHandle(connectionHandle);
dispatcher.deliver(appDis);
} catch (InvocationTargetException ex) {
logger.error(ex.getMessage(), ex);
throw new DispatcherException(ex);
}
}
/**
* 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.
*/
public TCTokenResponse handleActivate(TCTokenRequest request) {
final DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY);
boolean performChecks = TCTokenHacks.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());
dynCtx.put(TR03112Keys.TCTOKEN_URL, request.getTCTokenURL());
ConnectionHandleType connectionHandle = null;
TCTokenResponse response = new TCTokenResponse();
byte[] requestedContextHandle = request.getContextHandle();
String ifdName = request.getIFDName();
BigInteger requestedSlotIndex = request.getSlotIndex();
if (requestedContextHandle == null || ifdName == null || requestedSlotIndex == null) {
// use dumb activation without explicitly specifying the card and terminal
// see TR-03112-7 v 1.1.2 (2012-02-28) sec. 3.2
connectionHandle = getFirstHandle(request.getCardType());
} else {
// 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(ECardConstants.Minor.SAL.CANCELLATION_BY_USER, msg));
return response;
}
try {
// process binding and follow redirect addresses afterwards
response = processBinding(request, connectionHandle);
response = determineRefreshURL(request, response);
return response;
} catch (IOException w) {
logger.error(w.getMessage(), w);
// TODO: check for better matching minor type
response.setResult(WSHelper.makeResultUnknownError(w.getMessage()));
return response;
} catch (DispatcherException w) {
logger.error(w.getMessage(), w);
// TODO: check for better matching minor type
response.setResult(WSHelper.makeResultError(ECardConstants.Minor.App.INCORRECT_PARM, w.getMessage()));
return response;
} catch (PAOSException w) {
logger.error(w.getMessage(), w);
Throwable innerException = w.getCause();
if (innerException instanceof WSException) {
response.setResult(((WSException) innerException).getResult());
} else {
// TODO: check for better matching minor type
response.setResult(WSHelper.makeResultError(ECardConstants.Minor.App.INCORRECT_PARM, w.getMessage()));
}
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 of the TCToken as long as it 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 endpoint current Redirect location
* @return HTTP redirect to the final address the browser should be redirected to
* @throws HttpException
* @throws IOException
* @throws URISyntaxException
*/
private TCTokenResponse determineRefreshURL(TCTokenRequest request, TCTokenResponse response) throws IOException {
try {
URL endpoint = response.getRefreshAddress();
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).booleanValue() == true) {
return response;
}
// disable certificate checks according to BSI TR03112-7 in some situations
boolean redirectChecks = TCTokenHacks.isPerformTR03112Checks(request);
RedirectCertificateVerifier verifier = new RedirectCertificateVerifier(redirectChecks);
Pair<InputStream, List<Pair<URL, Certificate>>> result = TCTokenGrabber.getStream(endpoint, verifier);
// 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 = result.p2;
Pair<URL, Certificate> last = resultPoints.get(resultPoints.size() - 1);
endpoint = last.p1;
// we finally found the refresh URL; redirect the browser to this location, but first clear context
dynCtx.clear();
DynamicContext.remove();
logger.debug("Setting redirect address to '{}'.", endpoint);
response.setRefreshAddress(endpoint);
return response;
} catch (HttpException ex) {
throw new IOException(ex);
} catch (URISyntaxException ex) {
throw new IOException(ex);
}
}
}