- added SecurityUtils function getContractCertificateStatus() to check for certificate installation and update need within one function, thus reducing the cryptographic computation overhead (which is crucial on embedded systems)

- caching result of getContractCertificateStatus() in communication session object to further reduce cryptographic computation overhead
- added another property to file EVCCConfig.properties and SECCConfig.properties called 'XMLRepresentationOfMessages' to allow to easily switch between verbose debugging (showing XML representation of sent messages) and less verbose logging information
This commit is contained in:
Marc Mültin 2016-04-02 17:24:52 +02:00
parent 96ea627bb7
commit fbf547ffe7
8 changed files with 196 additions and 75 deletions

View File

@ -69,3 +69,14 @@ RequestedPaymentOption =
# - DC_combo_core
# - DC_unique
RequestedEnergyTransferMode = AC_three_phase_core
# XML representation of messages
#-------------------------------
#
# Possible values:
# - true
# - false
# If this value is set to 'true', the EXICodec will print each message's XML representation (for debugging purposes)
# If no correct value is provided here, 'false' will be chosen
XMLRepresentationOfMessages = false

View File

@ -10,7 +10,6 @@
*******************************************************************************/
package org.eclipse.risev2g.evcc.session;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
@ -49,7 +48,7 @@ import org.eclipse.risev2g.shared.messageHandling.SendMessage;
import org.eclipse.risev2g.shared.messageHandling.TerminateSession;
import org.eclipse.risev2g.shared.misc.V2GCommunicationSession;
import org.eclipse.risev2g.shared.misc.V2GTPMessage;
import org.eclipse.risev2g.shared.utils.SecurityUtils;
import org.eclipse.risev2g.shared.utils.SecurityUtils.ContractCertificateStatus;
import org.eclipse.risev2g.shared.v2gMessages.appProtocol.AppProtocolType;
import org.eclipse.risev2g.shared.v2gMessages.appProtocol.SupportedAppProtocolRes;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.ChargeParameterDiscoveryReqType;
@ -93,6 +92,7 @@ public class V2GCommunicationSessionEVCC extends V2GCommunicationSession impleme
private long saSchedulesReceived;
private CPStates changeToState; // signals a needed state change (checked when sending the request message)
private StatefulTransportLayerClient transportLayerClient;
private ContractCertificateStatus contractCertStatus;
public V2GCommunicationSessionEVCC(StatefulTransportLayerClient transportLayerClient) {
setTransportLayerClient(transportLayerClient);
@ -129,6 +129,9 @@ public class V2GCommunicationSessionEVCC extends V2GCommunicationSession impleme
* TODO check if this timing requirement is still up to date
*/
setV2gEVCCCommunicationSetupTimer(System.currentTimeMillis());
// Set default value for contract certificate status to UNKNOWN
setContractCertStatus(ContractCertificateStatus.UNKNOWN);
getLogger().debug("\n*******************************************" +
"\n* New V2G communication session initialized" +
@ -446,4 +449,14 @@ public class V2GCommunicationSessionEVCC extends V2GCommunicationSession impleme
return false;
}
public ContractCertificateStatus getContractCertStatus() {
return contractCertStatus;
}
public void setContractCertStatus(ContractCertificateStatus contractCertStatus) {
this.contractCertStatus = contractCertStatus;
}
}

View File

@ -11,14 +11,13 @@
package org.eclipse.risev2g.evcc.states;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import org.eclipse.risev2g.evcc.session.V2GCommunicationSessionEVCC;
import org.eclipse.risev2g.shared.enumerations.GlobalValues;
import org.eclipse.risev2g.shared.enumerations.V2GMessages;
import org.eclipse.risev2g.shared.messageHandling.ReactionToIncomingMessage;
import org.eclipse.risev2g.shared.messageHandling.TerminateSession;
import org.eclipse.risev2g.shared.utils.SecurityUtils;
import org.eclipse.risev2g.shared.utils.SecurityUtils.ContractCertificateStatus;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.CertificateInstallationReqType;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.CertificateUpdateReqType;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.PaymentOptionType;
@ -34,34 +33,27 @@ public class WaitForPaymentServiceSelectionRes extends ClientState {
public ReactionToIncomingMessage processIncomingMessage(Object message) {
if (isIncomingMessageValid(message, PaymentServiceSelectionResType.class)) {
if (getCommSessionContext().getSelectedPaymentOption().equals(PaymentOptionType.CONTRACT)) {
X509Certificate contractCert = SecurityUtils.getContractCertificate();
/*
* 1. Check if certificate installation is needed
* No valid contract certificate means:
* - no contract certificate is stored, or
* - existing contract certificates are expired or revoked
*/
if (contractCert == null || (contractCert != null && !SecurityUtils.isCertificateValid(contractCert))) {
if (getCommSessionContext().getContractCertStatus().equals(ContractCertificateStatus.UNKNOWN)) {
getCommSessionContext().setContractCertStatus(SecurityUtils.getContractCertificateStatus());
}
// 1. Check if certificate installation is needed
if (getCommSessionContext().getContractCertStatus().equals(ContractCertificateStatus.INSTALLATION_NEEDED)) {
if (getCommSessionContext().isCertificateServiceAvailable((short) 1)) {
if (contractCert == null) getLogger().info("No contract certificate stored, trying to install contract certificate");
else getLogger().info("Stored contract certificate not valid, trying to install new contract certificate");
getLogger().info("Trying to install new contract certificate");
return getSendMessage(getCertificateInstallationReq(), V2GMessages.CERTIFICATE_INSTALLATION_RES);
} else return new TerminateSession("Certificate installation needed but service is not available");
}
}
// 2. Check if certificate update is needed (means: certificate is available but expires soon)
short validityOfContractCert = SecurityUtils.getValidityPeriod(contractCert);
if (validityOfContractCert <= GlobalValues.CERTIFICATE_EXPIRES_SOON_PERIOD.getShortValue()) {
if (getCommSessionContext().getContractCertStatus().equals(ContractCertificateStatus.UPDATE_NEEDED)) {
if (getCommSessionContext().isCertificateServiceAvailable((short) 2)) {
getLogger().info("Stored contract certificate is about to expire in " + validityOfContractCert +
" days, trying to update contract certificate");
getLogger().info("Trying to update contract certificate");
return getSendMessage(getCertificateUpdateReq(), V2GMessages.CERTIFICATE_UPDATE_RES);
} else return new TerminateSession("Certificate update needed but service is not available");
}
}
return getSendMessage(getPaymentDetailsReq(), V2GMessages.PAYMENT_DETAILS_RES);
} else if (getCommSessionContext().getSelectedPaymentOption().equals(PaymentOptionType.EXTERNAL_PAYMENT)) {
return getSendMessage(getAuthorizationReq(null), V2GMessages.AUTHORIZATION_RES);

View File

@ -13,6 +13,7 @@ package org.eclipse.risev2g.evcc.states;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
import org.eclipse.risev2g.evcc.session.V2GCommunicationSessionEVCC;
import org.eclipse.risev2g.evcc.transportLayer.TLSClient;
import org.eclipse.risev2g.shared.enumerations.GlobalValues;
@ -21,6 +22,7 @@ import org.eclipse.risev2g.shared.messageHandling.ReactionToIncomingMessage;
import org.eclipse.risev2g.shared.messageHandling.TerminateSession;
import org.eclipse.risev2g.shared.utils.MiscUtils;
import org.eclipse.risev2g.shared.utils.SecurityUtils;
import org.eclipse.risev2g.shared.utils.SecurityUtils.ContractCertificateStatus;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.CertificateChainType;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.EnergyTransferModeType;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.PaymentOptionType;
@ -101,24 +103,12 @@ public class WaitForServiceDiscoveryRes extends ClientState {
if (getCommSessionContext().getTransportLayerClient() instanceof TLSClient) {
// Check if certificate service is needed
if (isCertificateServiceOffered(serviceDiscoveryRes.getServiceList())) {
KeyStore evccKeyStore = SecurityUtils.getKeyStore(
GlobalValues.EVCC_KEYSTORE_FILEPATH.toString(),
GlobalValues.PASSPHRASE_FOR_CERTIFICATES_AND_KEYS.toString());
getCommSessionContext().setContractCertStatus(SecurityUtils.getContractCertificateStatus());
CertificateChainType contractCertificateChain =
SecurityUtils.getCertificateChain(evccKeyStore, GlobalValues.ALIAS_CONTRACT_CERTIFICATE.toString());
if (contractCertificateChain != null) {
if (!SecurityUtils.isCertificateChainValid(contractCertificateChain)) {
addSelectedService(2, (short) 1);
} else {
if (isContractCertificateUpdateNeeded(contractCertificateChain)) {
addSelectedService(2, (short) 2);
}
}
} else {
if (getCommSessionContext().getContractCertStatus().equals(ContractCertificateStatus.INSTALLATION_NEEDED))
addSelectedService(2, (short) 1);
}
else if (getCommSessionContext().getContractCertStatus().equals(ContractCertificateStatus.UPDATE_NEEDED))
addSelectedService(2, (short) 2);
}
// Optionally, other value added services can be checked for here ...
@ -143,6 +133,7 @@ public class WaitForServiceDiscoveryRes extends ClientState {
getCommSessionContext().getServiceDetailsToBeRequested().add((short) serviceID);
}
private boolean isCertificateServiceOffered(ServiceListType offeredServiceList) {
for (ServiceType service : offeredServiceList.getService()) {
if (service.getServiceCategory().equals(ServiceCategoryType.CONTRACT_CERTIFICATE))
@ -151,21 +142,4 @@ public class WaitForServiceDiscoveryRes extends ClientState {
return false;
}
private boolean isContractCertificateUpdateNeeded(CertificateChainType contractCertificateChain) {
Date today = new Date();
X509Certificate contractCertificate = SecurityUtils.getCertificate(contractCertificateChain.getCertificate());
long validityDays = contractCertificate.getNotAfter().getTime() - today.getTime();
if (contractCertificate != null && validityDays <
( ((long) (int) MiscUtils.getPropertyValue("ContractCertificateUpdateTimespan")) * 24 * 60 * 60 * 1000 )) {
getLogger().info("Contract certificate with distinguished name '" +
contractCertificate.getSubjectX500Principal().getName() +
"' is only valid for " + validityDays / (1000 * 60 * 60 * 24) +
" days and needs to be updated");
return true;
} else return false;
}
}

View File

@ -63,3 +63,14 @@ SupportedPaymentOptions = Contract, ExternalPayment
# - false
PrivateEnvironment = false
# XML representation of messages
#-------------------------------
#
# Possible values:
# - true
# - false
# If this value is set to 'true', the EXICodec will print each message's XML representation (for debugging purposes)
# If no correct value is provided here, 'false' will be chosen
XMLRepresentationOfMessages = true

View File

@ -17,7 +17,6 @@ import java.io.InputStream;
import java.io.StringWriter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
@ -26,6 +25,7 @@ import javax.xml.bind.ValidationEventHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.risev2g.shared.utils.MiscUtils;
import org.eclipse.risev2g.shared.v2gMessages.appProtocol.SupportedAppProtocolReq;
import org.eclipse.risev2g.shared.v2gMessages.appProtocol.SupportedAppProtocolRes;
import org.eclipse.risev2g.shared.v2gMessages.msgDef.V2GMessage;
@ -39,6 +39,7 @@ public abstract class ExiCodec {
private InputStream inStream;
private Object decodedMessage;
private String decodedExi;
private boolean xmlRepresentation;
public ExiCodec() {
try {
@ -57,6 +58,12 @@ public abstract class ExiCodec {
event.getLinkedException());
}
});
// Check if XML representation of sent messages is to be shown (for debug purposes)
if ((boolean) MiscUtils.getPropertyValue("XMLRepresentationOfMessages"))
setXMLRepresentation(true);
else
setXMLRepresentation(false);
} catch (JAXBException e) {
getLogger().error("A JAXBException occurred while trying to instantiate " + this.getClass().getSimpleName(), e);
}
@ -72,24 +79,26 @@ public abstract class ExiCodec {
setInStream(new ByteArrayInputStream(baos.toByteArray()));
baos.close();
// For debugging purposes, you can view the XML representation of marshalled messages
StringWriter sw = new StringWriter();
String className = "";
if (jaxbObject instanceof V2GMessage) {
className = ((V2GMessage) jaxbObject).getBody().getBodyElement().getName().getLocalPart();
} else if (jaxbObject instanceof SupportedAppProtocolReq) {
className = "SupportedAppProtocolReq";
} else if (jaxbObject instanceof SupportedAppProtocolRes) {
className = "SupportedAppProtocolRes";
} else {
className = "marshalled JAXBElement";
if (isXMLRepresentation()) {
// For debugging purposes, you can view the XML representation of marshalled messages
StringWriter sw = new StringWriter();
String className = "";
if (jaxbObject instanceof V2GMessage) {
className = ((V2GMessage) jaxbObject).getBody().getBodyElement().getName().getLocalPart();
} else if (jaxbObject instanceof SupportedAppProtocolReq) {
className = "SupportedAppProtocolReq";
} else if (jaxbObject instanceof SupportedAppProtocolRes) {
className = "SupportedAppProtocolRes";
} else {
className = "marshalled JAXBElement";
}
getMarshaller().marshal(jaxbObject, sw);
getLogger().debug("XML representation of " + className + ":\n" + sw.toString());
sw.close();
}
getMarshaller().marshal(jaxbObject, sw);
getLogger().debug("XML representation of " + className + ":\n" + sw.toString());
sw.close();
return getInStream();
} catch (JAXBException | IOException e) {
getLogger().error(e.getClass().getSimpleName() + " occurred while trying to marshal to InputStream from JAXBElement", e);
@ -171,4 +180,13 @@ public abstract class ExiCodec {
public void setInStream(InputStream inStream) {
this.inStream = inStream;
}
private void setXMLRepresentation(boolean showXMLRepresentation) {
this.xmlRepresentation = showXMLRepresentation;
}
public boolean isXMLRepresentation() {
return xmlRepresentation;
}
}

View File

@ -212,6 +212,10 @@ public final class MiscUtils {
case "PrivateEnvironment": // EVSE property
returnValue = Boolean.parseBoolean(propertyValue);
break;
case "XMLRepresentationOfMessages": // EV + EVSE property
if (Boolean.parseBoolean(propertyValue)) returnValue = true;
else returnValue = false;
break;
default:
getLogger().error("No property with name '" + propertyName + "' found");
}

View File

@ -104,6 +104,13 @@ public final class SecurityUtils {
static Logger logger = LogManager.getLogger(SecurityUtils.class.getSimpleName());
static ExiCodec exiCodec;
public static enum ContractCertificateStatus {
UPDATE_NEEDED,
INSTALLATION_NEEDED,
OK,
UNKNOWN // is used as default for communication session context
}
public static Logger getLogger() {
return logger;
}
@ -825,9 +832,12 @@ public final class SecurityUtils {
keyStore.store(fos, GlobalValues.PASSPHRASE_FOR_CERTIFICATES_AND_KEYS.toString().toCharArray());
fos.close();
X509Certificate contractCert = getCertificate(contractCertChain.getCertificate());
getLogger().info("Contract certificate with distinguished name '" +
getCertificate(contractCertChain.getCertificate())
.getSubjectX500Principal().getName() + "' saved");
contractCert.getSubjectX500Principal().getName() + "' saved. " +
"Valid until " + contractCert.getNotAfter()
);
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | NullPointerException e) {
getLogger().error(e.getClass().getSimpleName() + " occurred while trying to save contract " +
"certificate chain", e);
@ -838,6 +848,11 @@ public final class SecurityUtils {
}
/**
* Gets the contract certificate from the EVCC keystore.
*
* @return The contract certificate if present, null otherwise
*/
public static X509Certificate getContractCertificate() {
X509Certificate contractCertificate = null;
@ -856,6 +871,89 @@ public final class SecurityUtils {
}
/**
* A convenience function which checks if a contract certificate installation is needed.
* Normally not needed because of function getContractCertificateStatus().
*
* @return True, if no contract certificate is store or if the stored certificate is not valid, false otherwise
*/
public static boolean isContractCertificateInstallationNeeded() {
X509Certificate contractCert = getContractCertificate();
if (contractCert == null) {
getLogger().info("No contract certificate stored");
return true;
} else if (contractCert != null && !isCertificateValid(contractCert)) {
getLogger().info("Stored contract certificate with distinguished name '" +
contractCert.getSubjectX500Principal().getName() + "' is not valid");
return true;
} else return false;
}
/**
* A convenience function which checks if a contract certificate update is needed.
* Normally not needed because of function getContractCertificateStatus().
*
* @return True, if contract certificate is still valid but about to expire, false otherwise.
* The expiration period is given in GlobalValues.CERTIFICATE_EXPIRES_SOON_PERIOD.
*/
public static boolean isContractCertificateUpdateNeeded() {
X509Certificate contractCert = getContractCertificate();
short validityOfContractCert = getValidityPeriod(contractCert);
if (validityOfContractCert < 0) {
getLogger().warn("Contract certificate with distinguished name '" +
contractCert.getSubjectX500Principal().getName() + "' is not valid any more, expired " +
Math.abs(validityOfContractCert) + " days ago");
return false;
} else if (validityOfContractCert <= GlobalValues.CERTIFICATE_EXPIRES_SOON_PERIOD.getShortValue()) {
getLogger().info("Contract certificate with distinguished name '" +
contractCert.getSubjectX500Principal().getName() + "' is about to expire in " +
validityOfContractCert + " days");
return true;
} else return false;
}
/**
* Checks whether a contract certificate
* - is stored
* - in case it is stored, if it is valid
* - in case it is valid, if it expires soon
*
* This method is intended to reduce cryptographic computation overhead by checking both, if installation or
* update is needed, at the same time. When executing either method by itself (isContractCertificateUpdateNeeded() and
* isContractCertificateInstallationNeeded()), each time the certificate is read anew from the Java keystore
* holding the contract certificate. With this method the contract certificate is read just once from the keystore.
*
* @return An enumeration value ContractCertificateStatus (either UPDATE_NEEDED, INSTALLATION_NEEDED, or OK)
*/
public static ContractCertificateStatus getContractCertificateStatus() {
X509Certificate contractCert = getContractCertificate();
if (contractCert == null) {
getLogger().info("No contract certificate stored");
return ContractCertificateStatus.INSTALLATION_NEEDED;
} else if (contractCert != null && !isCertificateValid(contractCert)) {
getLogger().info("Stored contract certificate with distinguished name '" +
contractCert.getSubjectX500Principal().getName() + "' is not valid");
return ContractCertificateStatus.INSTALLATION_NEEDED;
} else {
short validityOfContractCert = getValidityPeriod(contractCert);
// Checking for a negative value of validityOfContractCert is not needed because the method
// isCertificateValid() already checks for that
if (validityOfContractCert <= GlobalValues.CERTIFICATE_EXPIRES_SOON_PERIOD.getShortValue()) {
getLogger().info("Contract certificate with distinguished name '" +
contractCert.getSubjectX500Principal().getName() + "' is about to expire in " +
validityOfContractCert + " days");
return ContractCertificateStatus.UPDATE_NEEDED;
}
return ContractCertificateStatus.OK;
}
}
/**
* Returns a list of certificates from the given CertificateChainType with the leaf certificate
* being the first element and potential subcertificates (intermediate CA certificatess)