Revision 5

The Single-Port Protocol

The solartech single-port protocol is, in essence, a container protocol for the librarian protocol, the scheduler protocol, the source protocol, the display driver protocol, and the information daemon protocol. It is meant to be sent over a sequenced, reliable medium, so it does not handle issues like packetization, synchronization, error detection, retransmission, etc.

This protocol also adds user accounts, encryption, and logging.

The port for the single-port protocol is 1. That's right: 1. We actually implement the TCPMUX (RFC 1078) protocol for the start of the protocol. There are two services offered, which are the same protocol but with a different connection initiation:

  1. "solartech1"
  2. "solartech1-insecure"
  3. "solartech1-insecure-v10"
  4. "solartech2"

The secure version of the protocol should always be used over the internet (e.g. on a cell modem). The insecure version is provided for convenience on private networks such as LANs, secure wifi, and vpns.

If the protocol is solartech1-insecure, once the TCPMUX negotiation is finished, we move on to the solartech protocol initiation (described below). In the case of solartech1, the server will not send the TCPMUX response until the rest of the protocol initiation, so the client should send the secure protocol initiation packet immediately after sending the TCPMUX protocol request, without waiting for a response from the server.

"solartech1-insecure-v10" is the same as the solartech protocol, except that it uses version 10 of the secure protocol, rather than the latest version. (Support for this may be discontinued, and is only present to permit the NTCIP implementation to not have to catch up with the latest changes.

"solartech2" is the same as solartech1, except that the unit sends its MAC address with the rest of the handshake packet. This was meant for direct WiFi access to units, but was never used.

NOTE: in this documentation, the type "String" refers to a java string, which is the length specified as a big-endian short, followed by that many characters of UTF-8 data.

NOTE: all multi-byte data types, unless specified otherwise, are in big-endian (i.e. "Network") order.

NOTE: the AES encryption we use corresponds to the java cipher "AES/CFB8/NoPadding".

Connection Initiation (Secure)

The design of the secure initiation has two main goals:

  1. To prevent man-in-the-middle attacks, and to prevent replay attacks.
  2. To minimize both the amount of data sent as well as the initiation time. This means, in practice, minimizing the number of packets and especially minimizing the number of responses that either side must wait for.

(Because each connection has a unique key, the initialization vector is superfluous, and therefore is constant. The server→client initialization vector is 0x1f2e3d4c5b6a798897a6b5c4d3e2f100 and the client→server initialization vector is 0x0112233445566778899aabbccddeeff0.)

The client initiates the protocol with a Client Initiation Packet. The first field in it is a 16 byte salt in the form:

Client Initiation Packet
Byte Count Data Type Value Description
16 byte salt — The salt bytes.
[variable] String Username
11 ASCII data (bytes) "All is well" (encrypted) Confirmation string to make sure that the encryption was successful. (note: this is neither a java string, nor a null-terminated C string.)

There are several constraints on this salt, which exist to ensure its uniqueness in order to prevent replay attacks. The first 8 bytes of it must be the time, in UTC epoch milliseconds, encoded in big-endian order. This time must be within 5 minutes of what time the unit has. If it is not, the controller will respond negatively (described below) and indicate what time it has. That will allow the client to re-try with a time the unit will find acceptable.

The second constraint on the salt is that the second 8 bytes (bytes 8-15) must be unique to the controller for the last 5 minutes. In practice, they should probably be randomly chosen (they do not need crypto-strength randomness; the only purpose of the randomness is to ensure uniqueness). The mac-address of the client plus a 2-byte counter would also work reasonably well.

The password and username hashed together with the salt (using SHA-1) will be used as the 128 bit AES key to encrypt the session (note: the SHA-1 hash will be truncated to the first 128 bits (16 bytes) to form the key). The order is sha1(password, username, salt).

The server then responds, depending on whether the salt is acceptable and the confirmation string decrypted properly, with either a Server Initiation Packet or a Server Rejection Packet:

Server Initiation Packet
Byte Count Data Type Value Description
1 unsigned byte 0 Success
11 ASCII data (bytes) "All is well" (encrypted) Confirmation string to make sure that the encryption was successful. (note: this is neither a java string, nor a null-terminated C string.)
1 unsigned byte protocol version

(The server→client encryption begins after the status byte indicating success.)

The protocol version byte is used for the protocol versioning of the protocols embeded in this one. For various reasons, now mostly historical, the various protocols each have a version. With the secure single port protocol, those versions are being merged, starting at 8, so that every protocol is at the same version. Additionally, the version negotiation has been abandoned due to our policy that client software is required to be at the latest version. Instead, the server simply anounces what version of the protocol it speaks.

Server Rejection Packet
Byte Count Data Type Value Description
1 unsigned byte [1,2,3] Failure
1 time in the salt is not close enough to the unit's time
2 unique bytes in the salt have already been seen in the last 5 minutes
3 confirmation string did not properly decrypt
8 signed long time The current time (in UTC epoch milliseconds) according to the controller.

After sending a server rejection packet for reason 3 (bad confirmation string), the server will terminate the connection. In all other cases after sending a rejection packet, the client may try again (that is, send another Client Initiation Packet). The client will get up to 4 tries to send a successful Client Initiation packet before the server terminates the connection.

Connection Initiation (Insecure)

The design of the insecure initiation has two main goals:

  1. To make life easy on the client, with a minimum of library dependence.
  2. To still be reasonably secure, just in case.

The insecure initiation begins with an authentication packet from the client:

Authentication Packet
Byte Count Data Type Value Description
[variable] String Username
[variable] String Password

The server then responds indicating the success, and the protocol version:

Success Packet
Byte Count Data Type Value Description
1 unsigned byte [0,1] 0 == success, 1 == failure
1 unsigned byte protocol version

The protocol version byte is used for the protocol versioning of the protocols embeded in this one. For various reasons, now mostly historical, the various protocols each have a version. With the secure single port protocol, those versions are being merged, starting at 8, so that every protocol is at the same version. Additionally, the version negotiation has been abandoned due to our policy that client software is required to be at the latest version. Instead, the server simply anounces what version of the protocol it speaks. (NOTE: in the case of failure, the protocol version byte will be set to 0.)

In the case of failure, the server will then terminate the connection.

Normal Communications

All normal communications are encrypted using the AES encrypted negotiated during the initiation phase.

All communications afterwards consist of packets where the first byte is a channel prefix (see below), followed by a packet from some other protocol (the control protocol described below, or the librarian protocol, the scheduler protocol, the source protocol, the display driver protocol, and the information daemon protocol).

Terminating a connection

Connections are terminated by ending the connection via the underlying mechanism. Typically this is a TCP FIN packet (which the client will produce by calling close() on the relevant connection). The connection may also be severed by the server, by the same mechanism.

Channels

The important concept in the single port protocol is the channel. Channels correspond roughly to the old idea of protocols.

The particular channels are pre-assigned:

0 Control Channel
1 Information Daemon
2 Librarian
3 Event Daemon
4 SolarNet Control Protocol
5 SolarNet Librarian (i.e. for the organization library maintained on the SolarNet)
6 Arrowboard Control Protocol
16-31 Display Drivers
32-47 Schedulers

The Control Protocol

The control protocol is used primarily for changing users. It is communicated with over channel 0. If the initial authentication is made by the "root" user, then the change can be made without further authentication, otherwise the authentication version must be used.

Switch User Packet
Byte Count Data Type Value Description
1 unsigned byte 0 packet type
[variable] String Username

The server replies to a Switch User Packet with a Result Packet (see below)

Any user may issue an Authentication Packet, which has the same effect as the Switch User packet if the supplied password is correct:

Authentication Packet
Byte Count Data Type Value Description
1 unsigned byte 1 packet type
[variable] String Username
[variable] String Password

The server then returns a Result Packet:

Result Packet
Byte Count Data Type Value Description
1 unsigned byte 2 packet type
1 unsigned byte Result:
Value Meaning
0x00 Authentication Successful
0x01 Response Did Not Match
0x02 Internal Error

Users may be created or updated with a Create/Update User packet:

Create/Update User Packet
Byte Count Data Type Value Description
1 unsigned byte 3 packet type
[variable] String Username
[variable] String Password
1 unsigned byte role_count the number of roles the user has
variable String[role_count] The roles for the user
1 unsigned byte annotation_count Annotation count
variable String[2*annotation_count] The annotation names and values (interleaved, name first)
Notes: When creating a user, the password and roles must be specified, but no annotations are required. On update, if any of the fields have a count of zero (in the password's case, that corresponds to a String of ""), they should be left unchanged on the controller. A non-zero length is taken to replace the field. Thus when updating one role, all of the roles which the user has must be sent, and when updating one annotation, all of the annotations must be sent.

Users may be deleted with a Destroy User packet:

Destroy User Packet
Byte Count Data Type Value Description
1 unsigned byte 4 packet type
[variable] String Username

To get a list of accounts, send:

List User Packet
Byte Count Data Type Value Description
1 unsigned byte 5 packet type

The server than responds with:

User List Packet
Byte Count Data Type Value Description
1 unsigned byte 6 packet type
1 unsigned byte annotation_count the number of annotation fields on the users
variable String[annotation_count] The names of the annotations (e.g. "Full Name", "Email", etc.)
2 unsigned short user_count the number of user blocks in this packet
variable user blocks user_count user blocks
Where a user block is:
User Block
Byte Count Data Type Value Description
variable String Username
1 unsigned byte role_count the number of roles the user has
variable String[role_count] The roles for the user
variable String[annotation_count] The annotations for this user (full name, email address, etc.)

Any user may set a tag which will be added to every line logged. This is primarily to enable NTCIP to note which NTCIP user requests are being made on behalf of. The tag may be changed at any time.

Logging Tag Packet
Byte Count Data Type Value Description
1 unsigned byte 7 packet type
variable String The logging tag

A user can query their current roles by sending:

User Role Query Packet
Byte Count Data Type Value Description
1 unsigned byte 8 packet type
variable String username
Notes: If the username is blank, the currently logged in user is assumed.

The server then responds with a User Role Packet:
User Role Packet
Byte Count Data Type Value Description
1 unsigned byte 9 packet type
variable String username
1 unsigned byte role_count the number of roles the user has
variable String[role_count] The roles which the current user has
Notes: If the username is blank, the currently logged in user is assumed.