/*
* eID Applet Project.
* Copyright (C) 2014-2015 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.e_contract.eid.applet.service.impl.handler;
import java.io.IOException;
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.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.enterprise.event.Event;
import javax.inject.Inject;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import be.e_contract.eid.applet.service.impl.BeIDContextQualifier;
import be.e_contract.eid.applet.service.impl.Handles;
import be.fedict.eid.applet.service.Address;
import be.fedict.eid.applet.service.Identity;
import be.fedict.eid.applet.service.cdi.AuthenticatedEvent;
import be.fedict.eid.applet.service.cdi.AuthenticationEvent;
import be.fedict.eid.applet.service.cdi.IdentificationEvent;
import be.fedict.eid.applet.service.cdi.IdentityEvent;
import be.fedict.eid.applet.service.cdi.SecureChannelBindingEvent;
import be.fedict.eid.applet.service.cdi.SecurityAuditEvent;
import be.fedict.eid.applet.service.cdi.SecurityAuditEvent.Incident;
import be.fedict.eid.applet.service.impl.AuthenticationChallenge;
import be.fedict.eid.applet.service.impl.UserIdentifierUtil;
import be.fedict.eid.applet.service.impl.handler.MessageHandler;
import be.fedict.eid.applet.service.impl.tlv.TlvParser;
import be.fedict.eid.applet.service.spi.CertificateSecurityException;
import be.fedict.eid.applet.service.spi.ExpiredCertificateSecurityException;
import be.fedict.eid.applet.service.spi.RevokedCertificateSecurityException;
import be.fedict.eid.applet.service.spi.TrustCertificateSecurityException;
import be.fedict.eid.applet.shared.AuthenticationContract;
import be.fedict.eid.applet.shared.AuthenticationDataMessage;
import be.fedict.eid.applet.shared.ErrorCode;
import be.fedict.eid.applet.shared.FinishedMessage;
@Handles(AuthenticationDataMessage.class)
public class AuthenticationDataMessageHandler implements MessageHandler<AuthenticationDataMessage> {
@Inject
private Event<AuthenticationEvent> authenticationEvent;
@Inject
private Event<AuthenticatedEvent> authenticatedEvent;
@Inject
private Event<IdentityEvent> identityEvent;
@Inject
private Event<IdentificationEvent> identificationEvent;
@Inject
private Event<SecureChannelBindingEvent> secureChannelBindingEvent;
@Inject
private Event<SecurityAuditEvent> securityAuditEvent;
@Override
public Object handleMessage(AuthenticationDataMessage message, Map<String, String> httpHeaders,
HttpServletRequest request, HttpSession session) throws ServletException {
byte[] challenge;
try {
challenge = AuthenticationChallenge.getAuthnChallenge(session);
} catch (SecurityException e) {
throw new ServletException("security error: " + e.getMessage(), e);
}
/*
* We validate the authentication contract using the client-side
* communicated server SSL certificate in case of secure channel
* binding.
*/
AuthenticationContract authenticationContract = new AuthenticationContract(message.saltValue, null, null,
message.sessionId, message.encodedServerCertificate, challenge);
byte[] toBeSigned;
try {
toBeSigned = authenticationContract.calculateToBeSigned();
} catch (IOException e) {
throw new ServletException("IO error: " + e.getMessage(), e);
}
BeIDContextQualifier contextQualifier = new BeIDContextQualifier(request);
PublicKey signingKey = message.authnCert.getPublicKey();
byte[] signatureValue = message.signatureValue;
try {
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initVerify(signingKey);
signature.update(toBeSigned);
boolean result = signature.verify(signatureValue);
if (false == result) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.SIGNATURE, message.authnCert,
signatureValue);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new SecurityException("authn signature incorrect");
}
} catch (NoSuchAlgorithmException e) {
throw new SecurityException("algo error");
} catch (InvalidKeyException e) {
throw new SecurityException("authn key error");
} catch (SignatureException e) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.SIGNATURE, message.authnCert,
signatureValue);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new SecurityException("signature error");
}
List<X509Certificate> certificateChain = new LinkedList<X509Certificate>();
certificateChain.add(message.authnCert);
certificateChain.add(message.citizenCaCert);
certificateChain.add(message.rootCaCert);
AuthenticationEvent authenticationEvent = new AuthenticationEvent(certificateChain);
try {
this.authenticationEvent.select(contextQualifier).fire(authenticationEvent);
} 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);
}
if (false == authenticationEvent.isValid()) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.TRUST, message.authnCert);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new SecurityException("invalid authentication certificate chain");
}
if (null != message.encodedServerCertificate) {
SecureChannelBindingEvent secureChannelBindingEvent = new SecureChannelBindingEvent(
message.serverCertificate);
this.secureChannelBindingEvent.select(contextQualifier).fire(secureChannelBindingEvent);
if (false == secureChannelBindingEvent.isValid()) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.TRANSPORT,
message.serverCertificate);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new SecurityException("secure channel binding error");
}
}
if (null != message.identityData) {
List<X509Certificate> rrnCertificateChain = new LinkedList<X509Certificate>();
rrnCertificateChain.add(message.rrnCertificate);
rrnCertificateChain.add(message.rootCaCert);
IdentificationEvent identificationEvent = new IdentificationEvent(rrnCertificateChain);
this.identificationEvent.select(contextQualifier).fire(identificationEvent);
if (false == identificationEvent.isValid()) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.TRUST, message.rrnCertificate);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new SecurityException("invalid national registry certificate chain");
}
verifySignature(contextQualifier, message.rrnCertificate.getSigAlgName(), message.identitySignatureData,
message.rrnCertificate, request, message.identityData);
Identity identity = TlvParser.parse(message.identityData, Identity.class);
if (null != message.photoData) {
/*
* Photo integrity check.
*/
byte[] expectedPhotoDigest = identity.photoDigest;
byte[] actualPhotoDigest = digestPhoto(getDigestAlgo(expectedPhotoDigest.length), message.photoData);
if (false == Arrays.equals(expectedPhotoDigest, actualPhotoDigest)) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.DATA_INTEGRITY,
message.photoData);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new ServletException("photo digest incorrect");
}
}
Address address;
if (null != message.addressData) {
byte[] addressFile = trimRight(message.addressData);
verifySignature(contextQualifier, message.rrnCertificate.getSigAlgName(), message.addressSignatureData,
message.rrnCertificate, request, addressFile, message.identitySignatureData);
address = TlvParser.parse(message.addressData, Address.class);
} else {
address = null;
}
/*
* 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)) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.DATA_INTEGRITY,
message.identityData);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new SecurityException("eID card has expired");
}
}
String userId = UserIdentifierUtil.getUserId(message.authnCert);
if (!userId.equals(identity.getNationalNumber())) {
throw new SecurityException("mismatch between identity data and auth cert");
}
this.identityEvent.select(contextQualifier).fire(new IdentityEvent(identity, address, message.photoData, message.authnCert));
}
String userId = UserIdentifierUtil.getUserId(message.authnCert);
AuthenticatedEvent authenticatedEvent = new AuthenticatedEvent(userId);
this.authenticatedEvent.select(contextQualifier).fire(authenticatedEvent);
return new FinishedMessage();
}
@Override
public void init(ServletConfig config) throws ServletException {
}
private void verifySignature(BeIDContextQualifier contextQualifier, String signAlgo, byte[] signatureData,
X509Certificate certificate, HttpServletRequest request, byte[]... data) throws ServletException {
Signature signature;
try {
signature = Signature.getInstance(signAlgo);
} catch (NoSuchAlgorithmException e) {
throw new ServletException("algo error: " + e.getMessage(), e);
}
PublicKey publicKey = certificate.getPublicKey();
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) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.DATA_INTEGRITY, certificate,
signatureData);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new ServletException("signature incorrect");
}
} catch (SignatureException e) {
SecurityAuditEvent securityAuditEvent = new SecurityAuditEvent(Incident.DATA_INTEGRITY, certificate,
signatureData);
this.securityAuditEvent.select(contextQualifier).fire(securityAuditEvent);
throw new ServletException("signature error: " + e.getMessage(), e);
}
}
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");
}
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;
}
}