Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional RSU/SDX TIM Signing #67

Merged
merged 7 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,8 @@ The ODE is capable of communicating with RSUs to:
- Deposit TIMs
- Delete TIMs

By default, the ODE will not sign TIMs that are delivered to RSUs. This can be changed by setting the value of the DATA_SIGNING_ENABLED_RSU environment variable found in the provided sample.env file. Additionally, signing of SDX-delivery TIMs can be configured by setting the value of the environment variable DATA_SIGNING_ENABLED_SDW found in sample.env.

The following SNMP protocols are supported for communication with RSUs:
- DSRC 4.1 (defined in 'Dedicated Short-Range Communications Roadside Unit Specifications')
- NTCIP1218 (defined in 'National Transportation Communications for ITS Protocol')
Expand Down
2 changes: 2 additions & 0 deletions docker-compose-confluent-cloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ services:
KAFKA_TYPE: ${KAFKA_TYPE}
CONFLUENT_KEY: ${CONFLUENT_KEY}
CONFLUENT_SECRET: ${CONFLUENT_SECRET}
DATA_SIGNING_ENABLED_RSU: ${DATA_SIGNING_ENABLED_RSU}
DATA_SIGNING_ENABLED_SDW: ${DATA_SIGNING_ENABLED_SDW}
DEFAULT_SNMP_PROTOCOL: ${DEFAULT_SNMP_PROTOCOL}
# Commented out, will use SDW depositor module by default
#ODE_DEPOSIT_SDW_MESSAGES_OVER_WEBSOCKET: ${ODE_DEPOSIT_SDW_MESSAGES_OVER_WEBSOCKET}
Expand Down
2 changes: 2 additions & 0 deletions docker-compose-ppm-nsv.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ services:
ODE_SECURITY_SVCS_SIGNATURE_URI: ${ODE_SECURITY_SVCS_SIGNATURE_URI}
ODE_RSU_USERNAME: ${ODE_RSU_USERNAME}
ODE_RSU_PASSWORD: ${ODE_RSU_PASSWORD}
DATA_SIGNING_ENABLED_RSU: ${DATA_SIGNING_ENABLED_RSU}
DATA_SIGNING_ENABLED_SDW: ${DATA_SIGNING_ENABLED_SDW}
DEFAULT_SNMP_PROTOCOL: ${DEFAULT_SNMP_PROTOCOL}
# Commented out, will use SDW depositor module by default
#ODE_DEPOSIT_SDW_MESSAGES_OVER_WEBSOCKET: ${ODE_DEPOSIT_SDW_MESSAGES_OVER_WEBSOCKET}
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ services:
ODE_SECURITY_SVCS_SIGNATURE_URI: ${ODE_SECURITY_SVCS_SIGNATURE_URI}
ODE_RSU_USERNAME: ${ODE_RSU_USERNAME}
ODE_RSU_PASSWORD: ${ODE_RSU_PASSWORD}
DATA_SIGNING_ENABLED_RSU: ${DATA_SIGNING_ENABLED_RSU}
DATA_SIGNING_ENABLED_SDW: ${DATA_SIGNING_ENABLED_SDW}
DEFAULT_SNMP_PROTOCOL: ${DEFAULT_SNMP_PROTOCOL}
# Commented out, will use SDW depositor module by default
#ODE_DEPOSIT_SDW_MESSAGES_OVER_WEBSOCKET: ${ODE_DEPOSIT_SDW_MESSAGES_OVER_WEBSOCKET}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,6 @@ public void setVersion(String version) {
this.version = version;
}

public boolean dataSigningEnabled() {
return getSecuritySvcsSignatureUri() != null && !StringUtils.isEmptyOrWhitespace(getSecuritySvcsSignatureUri())
&& !getSecuritySvcsSignatureUri().startsWith("UNSECURE");
}

public List<Path> getUploadLocations() {
return this.uploadLocations;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public Asn1EncodedDataRouterException(String string) {
private OdeProperties odeProperties;
private MessageProducer<String, String> stringMsgProducer;
private Asn1CommandManager asn1CommandManager;
private boolean dataSigningEnabledRSU;
private boolean dataSigningEnabledSDW;

public Asn1EncodedDataRouter(OdeProperties odeProperties) {
super();
Expand All @@ -72,6 +74,12 @@ public Asn1EncodedDataRouter(OdeProperties odeProperties) {

this.asn1CommandManager = new Asn1CommandManager(odeProperties);

this.dataSigningEnabledRSU = System.getenv("DATA_SIGNING_ENABLED_RSU") != null && !System.getenv("DATA_SIGNING_ENABLED_RSU").isEmpty()
? Boolean.parseBoolean(System.getenv("DATA_SIGNING_ENABLED_RSU"))
: false;
this.dataSigningEnabledSDW = System.getenv("DATA_SIGNING_ENABLED_SDW") != null && !System.getenv("DATA_SIGNING_ENABLED_SDW").isEmpty()
? Boolean.parseBoolean(System.getenv("DATA_SIGNING_ENABLED_SDW"))
: true;
}

@Override
Expand All @@ -90,26 +98,28 @@ public Object process(String consumedData) {

if (metadata.has(TimTransmogrifier.REQUEST_STRING)) {
JSONObject request = metadata.getJSONObject(TimTransmogrifier.REQUEST_STRING);

if (request.has(TimTransmogrifier.RSUS_STRING)) {
JSONObject rsusIn = (JSONObject) request.get(TimTransmogrifier.RSUS_STRING);
if (rsusIn.has(TimTransmogrifier.RSUS_STRING)) {
Object rsu = rsusIn.get(TimTransmogrifier.RSUS_STRING);
JSONArray rsusOut = new JSONArray();
if (rsu instanceof JSONArray) {
logger.debug("Multiple RSUs exist in the request: {}", request);
JSONArray rsusInArray = (JSONArray) rsu;
for (int i = 0; i < rsusInArray.length(); i++) {
rsusOut.put(rsusInArray.get(i));
Object rsus = request.get(TimTransmogrifier.RSUS_STRING);
if (rsus instanceof JSONObject) {
JSONObject rsusIn = (JSONObject) request.get(TimTransmogrifier.RSUS_STRING);
if (rsusIn.has(TimTransmogrifier.RSUS_STRING)) {
Object rsu = rsusIn.get(TimTransmogrifier.RSUS_STRING);
JSONArray rsusOut = new JSONArray();
if (rsu instanceof JSONArray) {
Comment on lines +103 to +108
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would improve readability and comprehension for the reader if we extracted the blocks of these "if" statements into methods with descriptive names. Also, while it might be less impactful, extracting the conditions being checked into methods with descriptive names could also improve clarity. We should consider implementing these changes at some point.

logger.debug("Multiple RSUs exist in the request: {}", request);
JSONArray rsusInArray = (JSONArray) rsu;
for (int i = 0; i < rsusInArray.length(); i++) {
rsusOut.put(rsusInArray.get(i));
}
request.put(TimTransmogrifier.RSUS_STRING, rsusOut);
} else if (rsu instanceof JSONObject) {
logger.debug("Single RSU exists in the request: {}", request);
rsusOut.put(rsu);
request.put(TimTransmogrifier.RSUS_STRING, rsusOut);
} else {
logger.debug("No RSUs exist in the request: {}", request);
request.remove(TimTransmogrifier.RSUS_STRING);
}
request.put(TimTransmogrifier.RSUS_STRING, rsusOut);
} else if (rsu instanceof JSONObject) {
logger.debug("Single RSU exists in the request: {}", request);
rsusOut.put(rsu);
request.put(TimTransmogrifier.RSUS_STRING, rsusOut);
} else {
logger.debug("No RSUs exist in the request: {}", request);
request.remove(TimTransmogrifier.RSUS_STRING);
}
}
}
Expand Down Expand Up @@ -177,54 +187,9 @@ public void processEncodedTim(ServiceRequest request, JSONObject consumedObj) {
logger.debug("Encoded message - phase 1: {}", hexEncodedTim);
//use Asnc1 library to decode the encoded tim returned from ASNC1; another class two blockers: decode the tim and decode the message-sign

if (odeProperties.dataSigningEnabled()) {
dmccoystephenson marked this conversation as resolved.
Show resolved Hide resolved
logger.debug("Sending message for signature! ");
String base64EncodedTim = CodecUtils.toBase64(
CodecUtils.fromHex(hexEncodedTim));
JSONObject matadataObjs = consumedObj.getJSONObject(AppContext.METADATA_STRING);
// get max duration time and convert from minutes to milliseconds (unsigned
// integer valid 0 to 2^32-1 in units of
// milliseconds.) from metadata
int maxDurationTime = Integer.valueOf(matadataObjs.get("maxDurationTime").toString()) * 60 * 1000;
String timpacketID = matadataObjs.getString("odePacketID");
String timStartDateTime = matadataObjs.getString("odeTimStartDateTime");
String signedResponse = asn1CommandManager.sendForSignature(base64EncodedTim,maxDurationTime);
try {
hexEncodedTim = CodecUtils.toHex(
CodecUtils.fromBase64(
JsonUtils.toJSONObject(JsonUtils.toJSONObject(signedResponse).getString("result")).getString("message-signed")));

JSONObject TimWithExpiration = new JSONObject();
TimWithExpiration.put("packetID", timpacketID);
TimWithExpiration.put("startDateTime", timStartDateTime);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
try {
JSONObject jsonResult = JsonUtils
.toJSONObject((JsonUtils.toJSONObject(signedResponse).getString("result")));
// messageExpiry uses unit of seconds
long messageExpiry = Long.valueOf(jsonResult.getString("message-expiry"));
TimWithExpiration.put("expirationDate", dateFormat.format(new Date(messageExpiry * 1000)));
} catch (Exception e) {
logger.error("Unable to get expiration date from signed messages response {}", e);
TimWithExpiration.put("expirationDate", "null");
}

try {
Date parsedtimTimeStamp = dateFormat.parse(timStartDateTime);
Date requiredExpirationDate = new Date();
requiredExpirationDate.setTime(parsedtimTimeStamp.getTime() + maxDurationTime);
TimWithExpiration.put("requiredExpirationDate", dateFormat.format(requiredExpirationDate));
} catch (Exception e) {
logger.error("Unable to parse requiredExpirationDate {}", e);
TimWithExpiration.put("requiredExpirationDate", "null");
}
//publish to Tim expiration kafka
stringMsgProducer.send(odeProperties.getKafkaTopicSignedOdeTimJsonExpiration(), null,
TimWithExpiration.toString());

} catch (JsonUtilsException e1) {
logger.error("Unable to parse signed message response {}", e1);
}
// Case 1: SNMP-deposit
if (dataSigningEnabledRSU && request.getRsus() != null) {
hexEncodedTim = signTIM(hexEncodedTim, consumedObj);
}
else {
// if header is present, strip it
Expand All @@ -246,6 +211,13 @@ public void processEncodedTim(ServiceRequest request, JSONObject consumedObj) {
asn1CommandManager.sendToRsus(request, hexEncodedTim);
}

hexEncodedTim = mfObj.getString(BYTES);

// Case 2: SDX-deposit
if (dataSigningEnabledSDW && request.getSdw() != null) {
hexEncodedTim = signTIM(hexEncodedTim, consumedObj);
}

if (request.getSdw() != null) {
// Case 2 only

Expand All @@ -258,8 +230,7 @@ public void processEncodedTim(ServiceRequest request, JSONObject consumedObj) {
} else {
//We have encoded ASD. It could be either UNSECURED or secured.
logger.debug("securitySvcsSignatureUri = {}", odeProperties.getSecuritySvcsSignatureUri());

if (odeProperties.dataSigningEnabled()) {
if (dataSigningEnabledSDW && request.getSdw() != null) {
logger.debug("Signed message received. Depositing it to SDW.");
// We have a ASD with signed MessageFrame
// Case 3
Expand Down Expand Up @@ -353,6 +324,59 @@ public void processEncodedTimUnsecured(ServiceRequest request, JSONObject consum
logger.info("TIM deposit response {}", responseList);
}

public String signTIM(String encodedTIM, JSONObject consumedObj) {
logger.debug("Sending message for signature! ");
String base64EncodedTim = CodecUtils.toBase64(
CodecUtils.fromHex(encodedTIM));
JSONObject metadataObjs = consumedObj.getJSONObject(AppContext.METADATA_STRING);
// get max duration time and convert from minutes to milliseconds (unsigned
// integer valid 0 to 2^32-1 in units of
// milliseconds.) from metadata
int maxDurationTime = Integer.valueOf(metadataObjs.get("maxDurationTime").toString()) * 60 * 1000;
String timpacketID = metadataObjs.getString("odePacketID");
String timStartDateTime = metadataObjs.getString("odeTimStartDateTime");
String signedResponse = asn1CommandManager.sendForSignature(base64EncodedTim,maxDurationTime);
try {
String hexEncodedTim = CodecUtils.toHex(
CodecUtils.fromBase64(
JsonUtils.toJSONObject(JsonUtils.toJSONObject(signedResponse).getString("result")).getString("message-signed")));

JSONObject TimWithExpiration = new JSONObject();
dmccoystephenson marked this conversation as resolved.
Show resolved Hide resolved
TimWithExpiration.put("packetID", timpacketID);
TimWithExpiration.put("startDateTime", timStartDateTime);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
try {
JSONObject jsonResult = JsonUtils
.toJSONObject((JsonUtils.toJSONObject(signedResponse).getString("result")));
// messageExpiry uses unit of seconds
long messageExpiry = Long.valueOf(jsonResult.getString("message-expiry"));
TimWithExpiration.put("expirationDate", dateFormat.format(new Date(messageExpiry * 1000)));
} catch (Exception e) {
logger.error("Unable to get expiration date from signed messages response {}", e);
TimWithExpiration.put("expirationDate", "null");
}

try {
Date parsedtimTimeStamp = dateFormat.parse(timStartDateTime);
Date requiredExpirationDate = new Date();
requiredExpirationDate.setTime(parsedtimTimeStamp.getTime() + maxDurationTime);
TimWithExpiration.put("requiredExpirationDate", dateFormat.format(requiredExpirationDate));
} catch (Exception e) {
logger.error("Unable to parse requiredExpirationDate {}", e);
TimWithExpiration.put("requiredExpirationDate", "null");
}
//publish to Tim expiration kafka
stringMsgProducer.send(odeProperties.getKafkaTopicSignedOdeTimJsonExpiration(), null,
TimWithExpiration.toString());

return hexEncodedTim;

} catch (JsonUtilsException e1) {
logger.error("Unable to parse signed message response {}", e1);
}
return encodedTIM;
}

/**
* Checks if header is present in encoded message
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public class TimDepositController {
private MessageProducer<String, String> stringMsgProducer;
private MessageProducer<String, OdeObject> timProducer;

private boolean dataSigningEnabledSDW;

public static class TimDepositControllerException extends Exception {

private static final long serialVersionUID = 1L;
Expand All @@ -99,6 +101,10 @@ public TimDepositController(OdeProperties odeProperties) {
this.timProducer = new MessageProducer<>(odeProperties.getKafkaBrokers(), odeProperties.getKafkaProducerType(),
null, OdeTimSerializer.class.getName(), odeProperties.getKafkaTopicsDisabledSet());

this.dataSigningEnabledSDW = System.getenv("DATA_SIGNING_ENABLED_SDW") != null && !System.getenv("DATA_SIGNING_ENABLED_SDW").isEmpty()
? Boolean.parseBoolean(System.getenv("DATA_SIGNING_ENABLED_SDW"))
: true;

}

/**
Expand Down Expand Up @@ -226,7 +232,7 @@ public synchronized ResponseEntity<String> depositTim(String jsonString, Request
logger.debug("securitySvcsSignatureUri = {}", odeProperties.getSecuritySvcsSignatureUri());
String xmlMsg;
DdsAdvisorySituationData asd = null;
if (!odeProperties.dataSigningEnabled()) {
if (!this.dataSigningEnabledSDW) {
// We need to send data UNSECURED, so we should try to build the ASD as well as
// MessageFrame
asd = TimTransmogrifier.buildASD(odeTID.getRequest());
Expand Down
6 changes: 6 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ DOCKER_SHARED_VOLUME_WINDOWS=
ODE_RSU_USERNAME=
ODE_RSU_PASSWORD=

# Values to sign TIM messages delivered to RSUs/SDX (accepted values are true or false)
# If not set, DATA_SIGNING_ENABLED_RSU will default to false
# If not set, DATA_SIGNING_ENABLED_SDW will default to true
DATA_SIGNING_ENABLED_RSU=
DATA_SIGNING_ENABLED_SDW=

# Default SNMP protocol version when not specified in the request
# Current supported values are FOURDOT1 and NTCIP1218
# If no protocol is specified the FOURDOT1 protocol will be used
Expand Down
Loading