/*
* eID Applet Project.
* Copyright (C) 2008-2013 FedICT.
* Copyright (C) 2009-2014 e-Contract.be BVBA.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version
* 3.0 as published by the Free Software Foundation.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, see
* http://www.gnu.org/licenses/.
*/
package be.fedict.eid.applet.service.impl.handler;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Method;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import be.fedict.eid.applet.service.Address;
import be.fedict.eid.applet.service.EIdCertsData;
import be.fedict.eid.applet.service.EIdData;
import be.fedict.eid.applet.service.Identity;
import be.fedict.eid.applet.service.impl.RequestContext;
import be.fedict.eid.applet.service.impl.ServiceLocator;
import be.fedict.eid.applet.service.impl.tlv.TlvParser;
import be.fedict.eid.applet.service.spi.AuditService;
import be.fedict.eid.applet.service.spi.CertificateSecurityException;
import be.fedict.eid.applet.service.spi.ExpiredCertificateSecurityException;
import be.fedict.eid.applet.service.spi.IdentityIntegrityService;
import be.fedict.eid.applet.service.spi.RevokedCertificateSecurityException;
import be.fedict.eid.applet.service.spi.TrustCertificateSecurityException;
import be.fedict.eid.applet.shared.ErrorCode;
import be.fedict.eid.applet.shared.FinishedMessage;
import be.fedict.eid.applet.shared.IdentityDataMessage;
/**
* Message handler for the identity data message.
*
* @author Frank Cornelis
*
*/
@HandlesMessage(IdentityDataMessage.class)
public class IdentityDataMessageHandler implements MessageHandler<IdentityDataMessage> {
private static final Log LOG = LogFactory.getLog(IdentityDataMessageHandler.class);
public static final String IDENTITY_SESSION_ATTRIBUTE = "eid.identity";
public static final String ADDRESS_SESSION_ATTRIBUTE = "eid.address";
public static final String PHOTO_SESSION_ATTRIBUTE = "eid.photo";
public static final String EID_SESSION_ATTRIBUTE = "eid";
public static final String EID_CERTS_SESSION_ATTRIBUTE = "eid.certs";
public static final String AUTHN_CERT_SESSION_ATTRIBUTE = "eid.certs.authn";
public static final String SIGN_CERT_SESSION_ATTRIBUTE = "eid.certs.sign";
public static final String CA_CERT_SESSION_ATTRIBUTE = "eid.certs.ca";
/**
* Please use ROOT_CERT_SESSION_ATTRIBUTE instead.
*/
public static final String ROOT_CERT_SESSION_ATTRIBTUE = "eid.certs.root";
public static final String ROOT_CERT_SESSION_ATTRIBUTE = "eid.certs.root";
public static final String SKIP_NATIONAL_NUMBER_CHECK_INIT_PARAM_NAME = "SkipNationalNumberCheck";
public static final String INCLUDE_DATA_FILES = "IncludeDataFiles";
public static final String EID_DATA_IDENTITY_SESSION_ATTRIBUTE = "eid.data.identity";
public static final String EID_DATA_ADDRESS_SESSION_ATTRIBUTE = "eid.data.address";
@InitParam(SKIP_NATIONAL_NUMBER_CHECK_INIT_PARAM_NAME)
private boolean skipNationalNumberCheck;
@InitParam(HelloMessageHandler.IDENTITY_INTEGRITY_SERVICE_INIT_PARAM_NAME)
private ServiceLocator<IdentityIntegrityService> identityIntegrityServiceLocator;
@InitParam(AuthenticationDataMessageHandler.AUDIT_SERVICE_INIT_PARAM_NAME)
private ServiceLocator<AuditService> auditServiceLocator;
@InitParam(INCLUDE_DATA_FILES)
private boolean includeDataFiles;
public Object handleMessage(IdentityDataMessage message, Map<String, String> httpHeaders,
HttpServletRequest request, HttpSession session) throws ServletException {
LOG.debug("received identity data");
LOG.debug("identity file size: " + message.idFile.length);
// parse the identity files
Identity identity = TlvParser.parse(message.idFile, Identity.class);
RequestContext requestContext = new RequestContext(session);
boolean includeAddress = requestContext.includeAddress();
boolean includeCertificates = requestContext.includeCertificates();
boolean includePhoto = requestContext.includePhoto();
/*
* Check whether the answer is in-line with what we expected.
*/
Address address;
if (null != message.addressFile) {
LOG.debug("address file size: " + message.addressFile.length);
if (false == includeAddress) {
throw new ServletException("Address included while not requested");
}
/*
* Address file can be null.
*/
address = TlvParser.parse(message.addressFile, Address.class);
} else {
if (true == includeAddress) {
throw new ServletException("Address not included while requested");
}
address = null;
}
X509Certificate authnCert = null;
X509Certificate signCert = null;
X509Certificate caCert = null;
X509Certificate rootCert = null;
if (includeCertificates) {
if (null == message.authnCertFile) {
throw new ServletException("authn cert not included while requested");
}
if (null == message.signCertFile) {
throw new ServletException("sign cert not included while requested");
}
if (null == message.caCertFile) {
throw new ServletException("CA cert not included while requested");
}
if (null == message.rootCertFile) {
throw new ServletException("root cert not included while requested");
}
authnCert = getCertificate(message.authnCertFile);
signCert = getCertificate(message.signCertFile);
caCert = getCertificate(message.caCertFile);
rootCert = getCertificate(message.rootCertFile);
}
IdentityIntegrityService identityIntegrityService = this.identityIntegrityServiceLocator.locateService();
if (null != identityIntegrityService) {
/*
* First check if all required identity data is available.
*/
if (null == message.identitySignatureFile) {
throw new ServletException("identity signature data not included while request");
}
LOG.debug("identity signature file size: " + message.identitySignatureFile.length);
if (includeAddress) {
if (null == message.addressSignatureFile) {
throw new ServletException("address signature data not included while requested");
}
LOG.debug("address signature file size: " + message.addressSignatureFile.length);
}
if (null == message.rrnCertFile) {
throw new ServletException("national registry certificate not included while requested");
}
LOG.debug("RRN certificate file size: " + message.rrnCertFile.length);
/*
* Run identity integrity checks.
*/
X509Certificate rrnCertificate = getCertificate(message.rrnCertFile);
PublicKey rrnPublicKey = rrnCertificate.getPublicKey();
verifySignature(rrnCertificate.getSigAlgName(), message.identitySignatureFile, rrnPublicKey, request,
message.idFile);
if (false == this.skipNationalNumberCheck) {
String authnUserId = (String) session
.getAttribute(AuthenticationDataMessageHandler.AUTHENTICATED_USER_IDENTIFIER_SESSION_ATTRIBUTE);
if (null != authnUserId) {
if (false == authnUserId.equals(identity.nationalNumber)) {
throw new ServletException("national number mismatch");
}
}
}
if (includeAddress) {
byte[] addressFile = trimRight(message.addressFile);
verifySignature(rrnCertificate.getSigAlgName(), message.addressSignatureFile, rrnPublicKey, request,
addressFile, message.identitySignatureFile);
}
LOG.debug("checking national registration certificate: " + rrnCertificate.getSubjectX500Principal());
X509Certificate rootCertificate = getCertificate(message.rootCertFile);
List<X509Certificate> rrnCertificateChain = new LinkedList<X509Certificate>();
rrnCertificateChain.add(rrnCertificate);
rrnCertificateChain.add(rootCertificate);
try {
identityIntegrityService.checkNationalRegistrationCertificate(rrnCertificateChain);
} catch (ExpiredCertificateSecurityException e) {
return new FinishedMessage(ErrorCode.CERTIFICATE_EXPIRED);
} catch (RevokedCertificateSecurityException e) {
return new FinishedMessage(ErrorCode.CERTIFICATE_REVOKED);
} catch (TrustCertificateSecurityException e) {
return new FinishedMessage(ErrorCode.CERTIFICATE_NOT_TRUSTED);
} catch (CertificateSecurityException e) {
return new FinishedMessage(ErrorCode.CERTIFICATE);
} catch (Exception e) {
if ("javax.ejb.EJBException".equals(e.getClass().getName())) {
Exception exception;
try {
Method getCausedByExceptionMethod = e.getClass().getMethod("getCausedByException",
new Class[] {});
exception = (Exception) getCausedByExceptionMethod.invoke(e, new Object[] {});
} catch (Exception e2) {
LOG.debug("error: " + e.getMessage(), e);
throw new SecurityException("error retrieving the root cause: " + e2.getMessage());
}
if (exception instanceof ExpiredCertificateSecurityException) {
return new FinishedMessage(ErrorCode.CERTIFICATE_EXPIRED);
}
if (exception instanceof RevokedCertificateSecurityException) {
return new FinishedMessage(ErrorCode.CERTIFICATE_REVOKED);
}
if (exception instanceof TrustCertificateSecurityException) {
return new FinishedMessage(ErrorCode.CERTIFICATE_NOT_TRUSTED);
}
if (exception instanceof CertificateSecurityException) {
return new FinishedMessage(ErrorCode.CERTIFICATE);
}
}
throw new SecurityException("error checking the NRN certificate: " + e.getMessage(), e);
}
}
if (null != message.photoFile) {
LOG.debug("photo file size: " + message.photoFile.length);
if (false == includePhoto) {
throw new ServletException("photo include while not requested");
}
/*
* Photo integrity check.
*/
byte[] expectedPhotoDigest = identity.photoDigest;
byte[] actualPhotoDigest = digestPhoto(getDigestAlgo(expectedPhotoDigest.length), message.photoFile);
if (false == Arrays.equals(expectedPhotoDigest, actualPhotoDigest)) {
throw new ServletException("photo digest incorrect");
}
} else {
if (true == includePhoto) {
throw new ServletException("photo not included while requested");
}
}
/*
* Check the validity of the identity data as good as possible.
*/
GregorianCalendar cardValidityDateEndGregorianCalendar = identity.getCardValidityDateEnd();
if (null != cardValidityDateEndGregorianCalendar) {
Date now = new Date();
Date cardValidityDateEndDate = cardValidityDateEndGregorianCalendar.getTime();
if (now.after(cardValidityDateEndDate)) {
throw new SecurityException("eID card has expired");
}
}
// push the identity into the session
session.setAttribute(IDENTITY_SESSION_ATTRIBUTE, identity);
if (null != address) {
session.setAttribute(ADDRESS_SESSION_ATTRIBUTE, address);
}
if (null != message.photoFile) {
session.setAttribute(PHOTO_SESSION_ATTRIBUTE, message.photoFile);
}
if (includeCertificates) {
session.setAttribute(AUTHN_CERT_SESSION_ATTRIBUTE, authnCert);
session.setAttribute(SIGN_CERT_SESSION_ATTRIBUTE, signCert);
session.setAttribute(CA_CERT_SESSION_ATTRIBUTE, caCert);
session.setAttribute(ROOT_CERT_SESSION_ATTRIBUTE, rootCert);
}
EIdData eidData = (EIdData) session.getAttribute(EID_SESSION_ATTRIBUTE);
if (null == eidData) {
eidData = new EIdData();
session.setAttribute(EID_SESSION_ATTRIBUTE, eidData);
}
eidData.identity = identity;
eidData.address = address;
eidData.photo = message.photoFile;
if (includeCertificates) {
EIdCertsData eidCertsData = new EIdCertsData();
session.setAttribute(EID_CERTS_SESSION_ATTRIBUTE, eidCertsData);
eidData.certs = eidCertsData;
eidCertsData.authn = authnCert;
eidCertsData.sign = signCert;
eidCertsData.ca = caCert;
eidCertsData.root = rootCert;
session.setAttribute(AUTHN_CERT_SESSION_ATTRIBUTE, authnCert);
session.setAttribute(SIGN_CERT_SESSION_ATTRIBUTE, signCert);
session.setAttribute(CA_CERT_SESSION_ATTRIBUTE, caCert);
session.setAttribute(ROOT_CERT_SESSION_ATTRIBUTE, rootCert);
}
if (this.includeDataFiles) {
session.setAttribute(EID_DATA_IDENTITY_SESSION_ATTRIBUTE, message.idFile);
session.setAttribute(EID_DATA_ADDRESS_SESSION_ATTRIBUTE, message.addressFile);
}
AuditService auditService = this.auditServiceLocator.locateService();
if (null != auditService) {
String userId = identity.nationalNumber;
auditService.identified(userId);
}
return new FinishedMessage();
}
private byte[] trimRight(byte[] addressFile) {
int idx;
for (idx = 0; idx < addressFile.length; idx++) {
if (0 == addressFile[idx]) {
break;
}
}
byte[] result = new byte[idx];
System.arraycopy(addressFile, 0, result, 0, idx);
return result;
}
private void verifySignature(String signAlgo, byte[] signatureData, PublicKey publicKey, HttpServletRequest request,
byte[]... data) throws ServletException {
Signature signature;
try {
signature = Signature.getInstance(signAlgo);
} catch (NoSuchAlgorithmException e) {
throw new ServletException("algo error: " + e.getMessage(), e);
}
try {
signature.initVerify(publicKey);
} catch (InvalidKeyException e) {
throw new ServletException("key error: " + e.getMessage(), e);
}
try {
for (byte[] dataItem : data) {
signature.update(dataItem);
}
boolean result = signature.verify(signatureData);
if (false == result) {
AuditService auditService = this.auditServiceLocator.locateService();
if (null != auditService) {
String remoteAddress = request.getRemoteAddr();
auditService.identityIntegrityError(remoteAddress);
}
throw new ServletException("signature incorrect");
}
} catch (SignatureException e) {
AuditService auditService = this.auditServiceLocator.locateService();
if (null != auditService) {
String remoteAddress = request.getRemoteAddr();
auditService.identityIntegrityError(remoteAddress);
}
throw new ServletException("signature error: " + e.getMessage(), e);
}
}
/**
* Tries to parse the X509 certificate.
*
* @param certFile
* @return the X509 certificate, or <code>null</code> in case of a DER
* decoding error.
*/
private X509Certificate getCertificate(byte[] certFile) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
X509Certificate certificate = (X509Certificate) certificateFactory
.generateCertificate(new ByteArrayInputStream(certFile));
return certificate;
} catch (CertificateException e) {
LOG.warn("certificate error: " + e.getMessage(), e);
LOG.debug("certificate size: " + certFile.length);
LOG.debug("certificate file content: " + Hex.encodeHexString(certFile));
/*
* Missing eID authentication and eID non-repudiation certificates
* could become possible for future eID cards. A missing certificate
* is represented as a block of 1300 null bytes.
*/
if (1300 == certFile.length) {
boolean missingCertificate = true;
for (int idx = 0; idx < certFile.length; idx++) {
if (0 != certFile[idx]) {
missingCertificate = false;
}
}
if (missingCertificate) {
LOG.debug("the certificate data indicates a missing certificate");
}
}
return null;
}
}
private byte[] digestPhoto(String digestAlgoName, byte[] photoFile) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance(digestAlgoName);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("digest error: " + e.getMessage(), e);
}
byte[] photoDigest = messageDigest.digest(photoFile);
return photoDigest;
}
private String getDigestAlgo(final int hashSize) throws RuntimeException {
switch (hashSize) {
case 20:
return "SHA-1";
case 28:
return "SHA-224";
case 32:
return "SHA-256";
case 48:
return "SHA-384";
case 64:
return "SHA-512";
}
throw new RuntimeException("Failed to find guess algorithm for hash size of " + hashSize + " bytes");
}
public void init(ServletConfig config) throws ServletException {
// empty
}
}