/**************************************************************************** * Copyright (C) 2012-2015 ecsec GmbH. * 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 java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.net.URISyntaxException; import java.net.URL; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.openecard.apache.http.Header; import org.openecard.apache.http.HttpEntity; import org.openecard.apache.http.HttpException; import org.openecard.apache.http.HttpResponse; import org.openecard.apache.http.StatusLine; import org.openecard.apache.http.message.BasicHttpEntityEnclosingRequest; import org.openecard.apache.http.message.BasicHttpRequest; import org.openecard.apache.http.protocol.BasicHttpContext; import org.openecard.apache.http.protocol.HttpContext; import org.openecard.apache.http.protocol.HttpRequestExecutor; import static org.openecard.binding.tctoken.ex.ErrorTranslations.*; import org.openecard.binding.tctoken.ex.InvalidAddressException; import org.openecard.bouncycastle.crypto.tls.Certificate; import org.openecard.bouncycastle.crypto.tls.ProtocolVersion; import org.openecard.bouncycastle.crypto.tls.TlsClientProtocol; import org.openecard.common.DynamicContext; import org.openecard.common.I18n; import org.openecard.common.io.LimitedInputStream; import org.openecard.crypto.tls.proxy.ProxySettings; import org.openecard.common.util.FileUtils; import org.openecard.common.util.Pair; import org.openecard.common.util.Promise; import org.openecard.common.util.TR03112Utils; import org.openecard.crypto.common.ReusableSecureRandom; import org.openecard.transport.httpcore.cookies.CookieException; import org.openecard.transport.httpcore.cookies.CookieManager; import org.openecard.crypto.tls.ClientCertDefaultTlsClient; import org.openecard.crypto.tls.ClientCertTlsClient; import org.openecard.crypto.tls.auth.DynamicAuthentication; import org.openecard.crypto.tls.verify.JavaSecVerifier; import org.openecard.transport.httpcore.HttpRequestHelper; import org.openecard.transport.httpcore.HttpUtils; import org.openecard.transport.httpcore.InvalidResultStatus; import org.openecard.transport.httpcore.StreamHttpClientConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Implements a grabber to fetch TCTokens from an URL. * * @author Moritz Horsch * @author Johannes Schmölz * @author Tobias Wich */ public class ResourceContext { private static final Logger logger = LoggerFactory.getLogger(ResourceContext.class); private static final I18n lang = I18n.getTranslation("tr03112"); private final ClientCertTlsClient tlsClient; private final TlsClientProtocol tlsClientProto; private final List<Pair<URL, Certificate>> certs; private InputStream stream; private String data; protected ResourceContext(@Nullable ClientCertTlsClient tlsClient, @Nullable TlsClientProtocol tlsClientProto, @Nonnull List<Pair<URL, Certificate>> certs) { this.tlsClient = tlsClient; this.tlsClientProto = tlsClientProto; this.certs = certs; } public ClientCertTlsClient getTlsClient() { return tlsClient; } public TlsClientProtocol getTlsClientProto() { return tlsClientProto; } private void setStream(InputStream stream) { this.stream = stream; } public InputStream getStream() { return stream; } public void closeStream() { if (stream != null) { try { stream.close(); } catch (IOException ex) { logger.debug("Failed to close stream.", ex); } } if (tlsClientProto != null) { try { tlsClientProto.close(); } catch (IOException ex) { logger.debug("Failed to close connection.", ex); } } } public List<Pair<URL, Certificate>> getCerts() { return certs; } public synchronized String getData() throws IOException { // load data from stream first if (data == null) { try { data = FileUtils.toString(stream); } catch (IOException ex) { throw ex; } finally { if (stream != null) { try { stream.close(); } catch (IOException ex) { logger.debug("Failed to close stream.", ex); } } } } return data; } /** * Opens a stream to the given URL. * This function function uses {@link #getStream(URL, CertificateValidator)} to get the stream. A verifier which is * always true is used in the invocation. * * @param url URL pointing to the TCToken. * @return Resource as a stream and the server certificates plus chain received while retrieving the TC Token. * @throws IOException Thrown in case something went wrong in the connection layer. * @throws ResourceException Thrown when an unexpected condition (not TR-03112 conforming) occured. * @throws ValidationError The validator could not validate at least one host. * @throws InvalidAddressException */ public static ResourceContext getStream(URL url) throws IOException, ResourceException, ValidationError, InvalidAddressException { // use verifier which always returns return getStream(url, new CertificateValidator() { @Override public CertificateValidator.VerifierResult validate(URL url, Certificate cert) throws ValidationError { return CertificateValidator.VerifierResult.DONTCARE; } }); } /** * Opens a stream to the given URL. * This implementation follows redirects and records where it has been. * * @param url URL pointing to the TCToken. * @param v Certificate verifier instance. * @return Resource as a stream and the server certificates plus chain received while retrieving the TC Token. * @throws IOException Thrown in case something went wrong in the connection layer. * @throws ResourceException Thrown when an unexpected condition (not TR-03112 conforming) occured. * @throws ValidationError The validator could not validate at least one host. * @throws InvalidAddressException */ public static ResourceContext getStream(URL url, CertificateValidator v) throws IOException, ResourceException, ValidationError, InvalidAddressException { ArrayList<Pair<URL, Certificate>> serverCerts = new ArrayList<>(); return getStreamInt(url, v, serverCerts, 10); } private static ResourceContext getStreamInt(URL url, CertificateValidator v, List<Pair<URL, Certificate>> serverCerts, int maxRedirects) throws IOException, ResourceException, ValidationError, InvalidAddressException { try { DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY); CookieManager cManager = (CookieManager) dynCtx.get(TR03112Keys.COOKIE_MANAGER); logger.info("Trying to load resource from: {}", url); if (maxRedirects == 0) { throw new ResourceException(MAX_REDIRECTS); } maxRedirects--; String protocol = url.getProtocol(); String hostname = url.getHost(); int port = url.getPort(); if (port == -1) { port = url.getDefaultPort(); } String resource = url.getFile(); resource = resource.isEmpty() ? "/" : resource; if (! "https".equals(protocol)) { throw new InvalidAddressException(INVALID_ADDRESS); } // open a TLS connection, retrieve the server certificate and save it TlsClientProtocol h; DynamicAuthentication tlsAuth = new DynamicAuthentication(hostname); // add PKIX validator if not doin nPA auth if (isPKIXVerify()) { tlsAuth.addCertificateVerifier(new JavaSecVerifier()); } // FIXME: validate certificate chain as soon as a usable solution exists for the trust problem // tlsAuth.setCertificateVerifier(new JavaSecVerifier()); ClientCertTlsClient tlsClient = new ClientCertDefaultTlsClient(hostname, true); tlsClient.setAuthentication(tlsAuth); // connect tls client tlsClient.setClientVersion(ProtocolVersion.TLSv12); Socket socket = ProxySettings.getDefault().getSocket(hostname, port); SecureRandom sr = ReusableSecureRandom.getInstance(); h = new TlsClientProtocol(socket.getInputStream(), socket.getOutputStream(), sr); logger.debug("Performing TLS handshake."); h.connect(tlsClient); logger.debug("TLS handshake performed."); serverCerts.add(new Pair<>(url, tlsAuth.getServerCertificate())); // check result CertificateValidator.VerifierResult verifyResult = v.validate(url, tlsAuth.getServerCertificate()); if (verifyResult == CertificateValidator.VerifierResult.FINISH) { List<Pair<URL, Certificate>> pairs = Collections.unmodifiableList(serverCerts); return new ResourceContext(tlsClient, h, pairs); } StreamHttpClientConnection conn = new StreamHttpClientConnection(h.getInputStream(), h.getOutputStream()); HttpContext ctx = new BasicHttpContext(); HttpRequestExecutor httpexecutor = new HttpRequestExecutor(); BasicHttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("GET", resource); HttpRequestHelper.setDefaultHeader(req, url); req.setHeader("Accept", "text/xml, */*;q=0.8"); req.setHeader("Accept-Charset", "utf-8, *;q=0.8"); setCookieHeader(req, cManager, url); HttpUtils.dumpHttpRequest(logger, req); logger.debug("Sending HTTP request."); HttpResponse response = httpexecutor.execute(req, conn, ctx); storeCookies(response, cManager, url); logger.debug("HTTP response received."); StatusLine status = response.getStatusLine(); int statusCode = status.getStatusCode(); String reason = status.getReasonPhrase(); HttpUtils.dumpHttpResponse(logger, response, null); HttpEntity entity = null; boolean finished = false; if (TR03112Utils.isRedirectStatusCode(statusCode)) { Header[] headers = response.getHeaders("Location"); if (headers.length > 0) { String uri = headers[0].getValue(); url = new URL(uri); } else { // FIXME: refactor exception handling throw new ResourceException(MISSING_LOCATION_HEADER); } } else if (statusCode >= 400) { // according to the HTTP RFC, codes greater than 400 signal errors logger.debug("Received a result code {} '{}' from server.", statusCode, reason); throw new InvalidResultStatus(lang.translationForKey(INVALID_RESULT_STATUS, statusCode, reason)); } else { if (verifyResult == CertificateValidator.VerifierResult.CONTINUE) { throw new InvalidAddressException(INVALID_REFRESH_ADDRESS_NOSOP); } else { conn.receiveResponseEntity(response); entity = response.getEntity(); finished = true; } } // follow next redirect or finish? if (finished) { assert(entity != null); ResourceContext result = new ResourceContext(tlsClient, h, serverCerts); LimitedInputStream is = new LimitedInputStream(entity.getContent()); result.setStream(is); return result; } else { h.close(); return getStreamInt(url, v, serverCerts, maxRedirects); } } catch (URISyntaxException ex) { throw new IOException(lang.translationForKey(FAILED_PROXY), ex); } catch (HttpException ex) { // don't translate this, it is handled in the ActivationAction throw new IOException("Invalid HTTP message received.", ex); } } /** * Get the {@code Cookie} header for the given {@link URL} and from the given {@link CookieManager}. * * @param manager {@link CookieManager} used to manage the cookies and getting the value of the {@code Cookie} header. * @param url {@link URL} for which the {@code Cookie} header shall be set. * @return The value of the {@code Cookie} header or {@code NULL} if there exist not cookies for the given {@link URL}. */ @Nullable private static String setCookieHeader(@Nonnull BasicHttpRequest req, CookieManager manager, @Nonnull URL url) { String cookieHeader = null; try { if (manager != null) { cookieHeader = manager.getCookieHeaderValue(url.toString()); if (cookieHeader != null && !cookieHeader.isEmpty()) { req.setHeader("Cookie", cookieHeader); } } } catch (CookieException ex) { // ignore because the input parameter is created from a valid URL. } return cookieHeader; } /** * Stores the cookies contained in the given HttpResponse. * If there are no {@code Set-Cookie} headers in the request nothing will be stored. * * @param response Http Response containing possible {@code Set-Cookie} headers. * @param cManager {@link CookieManager} to use for managing the cookies. * @param url URL which was called. */ private static void storeCookies(@Nonnull HttpResponse response, CookieManager cManager, @Nonnull URL url) { Header[] headers = response.getAllHeaders(); for (Header header : headers) { if (header.getName().toLowerCase().equals("set-cookie")) { try { if (cManager != null) { cManager.addCookie(url.toString(), header.getValue()); } } catch(CookieException ex) { String msg = "Received invalid cookie from: %s. The cookie is not stored."; msg = String.format(msg, url.toString()); logger.warn(msg, ex); } } } } private static boolean isPKIXVerify() { DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY); Promise<Object> cardTypeP = dynCtx.getPromise(TR03112Keys.ACTIVATION_CARD_TYPE); Object cardType = cardTypeP.derefNonblocking(); // verify when the value is not set or when no nPA is requested if (cardType != null && ! "http://bsi.bund.de/cif/npa.xml".equals(cardType)) { return true; } else { return false; } } }