diff --git a/RISE-V2G-EVCC/EVCCConfig.properties b/RISE-V2G-EVCC/EVCCConfig.properties index 07c16de..812047a 100644 --- a/RISE-V2G-EVCC/EVCCConfig.properties +++ b/RISE-V2G-EVCC/EVCCConfig.properties @@ -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 diff --git a/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/session/V2GCommunicationSessionEVCC.java b/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/session/V2GCommunicationSessionEVCC.java index f943feb..9a4fe33 100644 --- a/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/session/V2GCommunicationSessionEVCC.java +++ b/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/session/V2GCommunicationSessionEVCC.java @@ -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; + } + } diff --git a/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForPaymentServiceSelectionRes.java b/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForPaymentServiceSelectionRes.java index dd5b2f0..a84f4b3 100644 --- a/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForPaymentServiceSelectionRes.java +++ b/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForPaymentServiceSelectionRes.java @@ -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); diff --git a/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForServiceDiscoveryRes.java b/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForServiceDiscoveryRes.java index 7f62adb..c45676f 100644 --- a/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForServiceDiscoveryRes.java +++ b/RISE-V2G-EVCC/src/main/java/org/eclipse/risev2g/evcc/states/WaitForServiceDiscoveryRes.java @@ -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; - } } diff --git a/RISE-V2G-SECC/SECCConfig.properties b/RISE-V2G-SECC/SECCConfig.properties index a1813d8..fc39eb5 100644 --- a/RISE-V2G-SECC/SECCConfig.properties +++ b/RISE-V2G-SECC/SECCConfig.properties @@ -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 + diff --git a/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/exiCodec/ExiCodec.java b/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/exiCodec/ExiCodec.java index bd77a92..4aa4c38 100644 --- a/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/exiCodec/ExiCodec.java +++ b/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/exiCodec/ExiCodec.java @@ -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; + } } diff --git a/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/MiscUtils.java b/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/MiscUtils.java index 4ca9d41..72072be 100644 --- a/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/MiscUtils.java +++ b/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/MiscUtils.java @@ -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"); } diff --git a/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/SecurityUtils.java b/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/SecurityUtils.java index ed7bc04..130cad3 100644 --- a/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/SecurityUtils.java +++ b/RISE-V2G-Shared/src/main/java/org/eclipse/risev2g/shared/utils/SecurityUtils.java @@ -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)