/****************************************************************************
* 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 org.openecard.binding.tctoken.ex.InvalidTCTokenElement;
import org.openecard.binding.tctoken.ex.InvalidTCTokenUrlException;
import org.openecard.binding.tctoken.ex.SecurityViolationException;
import org.openecard.binding.tctoken.ex.InvalidRedirectUrlException;
import generated.TCTokenType;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import javax.annotation.Nonnull;
import org.openecard.binding.tctoken.ex.ActivationError;
import org.openecard.binding.tctoken.ex.InvalidAddressException;
import org.openecard.bouncycastle.crypto.tls.Certificate;
import org.openecard.common.util.Pair;
import org.openecard.common.util.TR03112Utils;
import static org.openecard.binding.tctoken.ex.ErrorTranslations.*;
import org.openecard.binding.tctoken.ex.ResultMinor;
import org.openecard.binding.tctoken.ex.UserCancellationException;
import org.openecard.common.DynamicContext;
import org.openecard.common.util.UrlBuilder;
/**
* Implements a verifier to check the elements of a TCToken.
*
* @author Moritz Horsch
* @author Tobias Wich
* @author Hans-Martin Haase
*/
public class TCTokenVerifier {
private final TCToken token;
private final ResourceContext ctx;
/**
* Creates a new TCTokenVerifier to verify a TCToken.
*
* @param token Token
* @param ctx Context over which the token has been received.
*/
public TCTokenVerifier(@Nonnull TCToken token, ResourceContext ctx) {
this.token = token;
this.ctx = ctx;
}
/**
* Checks if the token is a response to an error.
* These kind of token only contain the CommunicationErrorAdress field.
*
* @return {@code true} if this is an error token, {@code false} otherwise.
*/
public boolean isErrorToken() {
if (token.getCommunicationErrorAddress() != null) {
// refresh address is essential, if that one is missing, it must be an error token
return token.getRefreshAddress() == null;
}
return false;
}
/**
* Verifies the elements of the TCToken.
*
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case one of the values to test is errornous.
* @throws InvalidTCTokenUrlException Thrown in case a tested URL does not conform to the specification.
* @throws SecurityViolationException Thrown in case the same origin policy is violated.
* @throws UserCancellationException Thrown in case the user aborted the insert card dialog.
*/
public void verify() throws InvalidRedirectUrlException, InvalidTCTokenElement, InvalidTCTokenUrlException,
SecurityViolationException, UserCancellationException {
// ordering is important because of the raised errors in case the first two are not https URLs
initialCheck();
verifyRefreshAddress();
verifyCommunicationErrorAddress();
checkUserCancellation();
verifyServerAddress();
verifySessionIdentifier();
verifyBinding();
verifyPathSecurity();
}
/**
* Verifies the ServerAddress element of the TCToken.
*
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case one of the values to test is errornous.
* @throws UserCancellationException Thrown in case the user aborted the insert card dialog but this should never
* happen in this case.
*/
public void verifyServerAddress() throws InvalidRedirectUrlException, InvalidTCTokenElement,
UserCancellationException {
String value = token.getServerAddress();
try {
assertRequired("ServerAddress", value);
} catch (InvalidTCTokenElement ex) {
determineRefreshAddress(ex);
}
try {
assertHttpsURL("ServerAddress", value);
} catch (InvalidTCTokenUrlException ex) {
determineRefreshAddress(ex);
}
}
/**
* Verifies the SessionIdentifier element of the TCToken.
*
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case one of the values to test is errornous.
*/
public void verifySessionIdentifier() throws InvalidRedirectUrlException, InvalidTCTokenElement {
String value = token.getSessionIdentifier();
assertRequired("SessionIdentifier", value);
}
/**
* Verifies the RefreshAddress element of the TCToken.
*
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case one of the values to test is errornous.
* @throws UserCancellationException Thrown in case the user aborted the insert card dialog but this should never
* happen in this case.
*/
public void verifyRefreshAddress() throws InvalidRedirectUrlException, InvalidTCTokenElement,
UserCancellationException {
String value = token.getRefreshAddress();
assertRequired("RefreshAddress", value);
try {
assertHttpsURL("RefreshAddress", value);
} catch (InvalidTCTokenUrlException ex) {
determineRefreshAddress(ex);
}
}
/**
* Verifies the CommunicationErrorAddress element of the TCToken.
*
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case one of the values to test is errornous.
* @throws InvalidTCTokenUrlException Thrown in case a tested URL does not conform to the specification.
*/
public void verifyCommunicationErrorAddress() throws InvalidRedirectUrlException, InvalidTCTokenElement,
InvalidTCTokenUrlException {
String value = token.getCommunicationErrorAddress();
if (! checkEmpty(value)) {
assertRequired("CommunicationErrorAddress", value);
assertHttpsURL("CommunicationErrorAddress", value);
}
}
/**
* Verifies the Binding element of the TCToken.
*
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case one of the values to test is errornous.
*/
public void verifyBinding() throws InvalidRedirectUrlException, InvalidTCTokenElement {
String value = token.getBinding();
assertRequired("Binding", value);
checkEqualOR("Binding", value, "urn:liberty:paos:2006-08", "urn:ietf:rfc:2616");
}
/**
* Verifies the PathSecurity-Protocol and PathSecurity-Parameters element of the TCToken.
*
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case one of the values to test is errornous.
* @throws InvalidTCTokenUrlException Thrown in case a tested URL does not conform to the specification.
* @throws SecurityViolationException Thrown in case the same origin policy is violated.
* @throws UserCancellationException Thrown in case the user aborted the insert card dialog but this should never
* happen in this case.
*/
public void verifyPathSecurity() throws InvalidRedirectUrlException, InvalidTCTokenElement, InvalidTCTokenUrlException,
SecurityViolationException, UserCancellationException {
String proto = token.getPathSecurityProtocol();
TCTokenType.PathSecurityParameters psp = token.getPathSecurityParameters();
// TR-03124 sec. 2.4.3
// If no PathSecurity-Protocol/PSK is given in the TC Token, the same TLS channel as established to
// retrieve the TC Token MUST be used for the PAOS connection, i.e. a new channel MUST NOT be established.
if ((checkEmpty(proto) && checkEmpty(psp))) {
assertSameChannel("ServerAddress", token.getServerAddress());
return;
} else if ((! checkEmpty(proto)) && (! checkEmpty(psp)) && checkEmpty(psp.getPSK()) && ! token.isInvalidPSK()) {
assertSameChannel("ServerAddress", token.getServerAddress());
return;
}
assertRequired("PathSecurityProtocol", proto);
String[] protos = {"urn:ietf:rfc:4346", "urn:ietf:rfc:5246", "urn:ietf:rfc:4279"};
checkEqualOR("PathSecurityProtocol", proto, protos);
if ("urn:ietf:rfc:4279".equals(proto)) {
try {
assertRequired("PathSecurityParameters", psp);
} catch (InvalidTCTokenElement ex) {
determineRefreshAddress(ex);
}
try {
assertRequired("PSK", psp.getPSK());
} catch (InvalidTCTokenElement ex) {
determineRefreshAddress(ex);
}
}
}
/**
* Checks if the value is "empty".
*
* @param value Value
* @return True if the element is empty, otherwise false
*/
private boolean checkEmpty(Object value) {
if (value != null) {
if (value instanceof String) {
if (((String) value).isEmpty()) {
return true;
}
} else if (value instanceof URL) {
if (((URL) value).toString().isEmpty()) {
return true;
}
} else if (value instanceof byte[]) {
if (((byte[]) value).length == 0) {
return true;
}
}
return false;
}
return true;
}
/**
* Checks the value for equality against any of the given reference values.
*
* @param name Name of the element to check. This value is used to provide a concise error message.
* @param value Value to test.
* @param reference Reference values to test equality against.
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case the value is not equal to any of the reference values.
*/
private void checkEqualOR(String name, String value, String... reference) throws InvalidRedirectUrlException,
InvalidTCTokenElement {
for (String string : reference) {
if (value.equals(string)) {
return;
}
}
String minor = ResultMinor.COMMUNICATION_ERROR;
String errorUrl = token.getComErrorAddressWithParams(minor);
throw new InvalidTCTokenElement(errorUrl, INVALID_ELEMENT, (Object) name);
}
/**
* Checks if the element is present.
*
* @param name Name of the element to check. This value is used to provide a concise error message.
* @param value Value to test.
* @throws InvalidRedirectUrlException Thrown in case no redirect URL could be determined.
* @throws InvalidTCTokenElement Thrown in case the value is null or empty.
*/
private void assertRequired(String name, Object value) throws InvalidRedirectUrlException, InvalidTCTokenElement {
if (checkEmpty(value)) {
String minor = ResultMinor.COMMUNICATION_ERROR;
String errorUrl = token.getComErrorAddressWithParams(minor);
throw new InvalidTCTokenElement(errorUrl, MISSING_ELEMENT, name);
}
}
private URL assertURL(String name, String value) throws InvalidTCTokenUrlException {
try {
return new URL(value);
} catch (MalformedURLException e) {
throw new InvalidTCTokenUrlException(MALFORMED_URL, name);
}
}
private URL assertHttpsURL(String name, String value) throws InvalidTCTokenUrlException {
URL url = assertURL(name, value);
if (! "https".equals(url.getProtocol())) {
throw new InvalidTCTokenUrlException(NO_HTTPS_URL, name);
} else {
return url;
}
}
private void assertSameChannel(String name, String address) throws InvalidRedirectUrlException,
InvalidTCTokenUrlException, SecurityViolationException {
// check that everything can be handled over the same channel
// TR-03124-1 does not mention that redirects on the TCToken address are possible and it also states that there
// are only two channels. So I guess we should force this here as well.
URL paosUrl = assertURL(name, address);
List<Pair<URL, Certificate>> urls = ctx.getCerts();
for (Pair<URL, Certificate> next : urls) {
if (! TR03112Utils.checkSameOriginPolicy(paosUrl, next.p1)) {
String minor = ResultMinor.COMMUNICATION_ERROR;
String errorUrl = token.getComErrorAddressWithParams(minor);
throw new SecurityViolationException(errorUrl, FAILED_SOP);
}
}
}
/**
* Creates an{@link URL} with a communication error as parameters and also a given error message.
*
* @param refreshAddress The address which should get the error parameters.
* @param minor The result minor to attach to the {@code refreshAddress}.
* @param minorMessage The result message.
* @return An {@link URL} object containing the error query parameters.
* @throws URISyntaxException Thrown if the given {@code refreshAddress} is not a valid URL.
*/
private URI createUrlWithErrorParams(String refreshAddress, String minor, String minorMessage)
throws URISyntaxException {
return UrlBuilder.fromUrl(refreshAddress)
.queryParam("ResultMajor", "error")
.queryParamUrl("ResultMinor", TCTokenHacks.fixResultMinor(minor))
.queryParam("ResultMessage", minorMessage)
.build();
}
/**
* Checks whether the TCToken is empty except the CommunicationErrorAddress.
*
* @throws InvalidRedirectUrlException
* @throws InvalidTCTokenElement
*/
private void initialCheck() throws InvalidRedirectUrlException, InvalidTCTokenElement {
if (token.getCommunicationErrorAddress() != null && ! token.getCommunicationErrorAddress().isEmpty() &&
token.getRefreshAddress().isEmpty() && token.getServerAddress().isEmpty() &&
token.getSessionIdentifier().isEmpty() && token.getBinding().isEmpty() &&
token.getPathSecurityProtocol().isEmpty()) {
String errorUrl = token.getComErrorAddressWithParams(ResultMinor.COMMUNICATION_ERROR);
throw new InvalidTCTokenElement(errorUrl, ESERVICE_FAIL);
}
}
/**
* Determines the refresh URL.
*
* @param ex The exception which caused the abort of the TCToken verification.
* @throws InvalidRedirectUrlException If the CommunicationErrorAddress cant be determined.
* @throws InvalidTCTokenElement If a determination of a refresh or CommunicationError address was successful.
* @throws UserCancellationException Thrown in case {@code ex} is an instance of {@link UserCancellationException}.
*/
private void determineRefreshAddress(ActivationError ex) throws InvalidRedirectUrlException,
InvalidTCTokenElement, UserCancellationException {
if (token.getRefreshAddress() != null) {
try {
CertificateValidator validator = new RedirectCertificateValidator(true);
ResourceContext newResCtx = ResourceContext.getStream(new URL(token.getRefreshAddress()), validator);
newResCtx.closeStream();
List<Pair<URL, Certificate>> resultPoints = newResCtx.getCerts();
Pair<URL, Certificate> last = resultPoints.get(resultPoints.size() - 1);
URL resAddr = last.p1;
String refreshUrl = resAddr.toString();
if (ex instanceof UserCancellationException) {
UserCancellationException uex = (UserCancellationException) ex;
URI refreshUrlAsUrl = createUrlWithErrorParams(refreshUrl, ResultMinor.CANCELLATION_BY_USER,
ex.getMessage());
throw new UserCancellationException(refreshUrlAsUrl.toString(), ex);
}
URI refreshUrlAsUrl = createUrlWithErrorParams(refreshUrl,
ResultMinor.TRUSTED_CHANNEL_ESTABLISCHMENT_FAILED, ex.getMessage());
throw new InvalidTCTokenElement(refreshUrlAsUrl.toString(), ex);
} catch (IOException | ResourceException | InvalidAddressException | ValidationError | URISyntaxException ex1) {
String errorUrl = token.getComErrorAddressWithParams(ResultMinor.COMMUNICATION_ERROR);
throw new InvalidTCTokenElement(errorUrl, INVALID_REFRESH_ADDRESS, ex1);
}
} else {
String errorUrl = token.getComErrorAddressWithParams(ResultMinor.COMMUNICATION_ERROR);
throw new InvalidTCTokenElement(errorUrl, NO_REFRESH_ADDRESS);
}
}
private void checkUserCancellation() throws InvalidRedirectUrlException, InvalidTCTokenElement,
UserCancellationException {
DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY);
UserCancellationException ex = (UserCancellationException) dynCtx.get(TR03112Keys.CARD_SELECTION_CANCELLATION);
if (ex != null) {
determineRefreshAddress(ex);
}
}
}