blob: 2f3ee9b5430271dafe8d76fb2cdf609c10f3f832 [file] [log] [blame] [view]
# Roughtime protocol
## Messages
Roughtime messages are a map from uint32s to byte-strings. The number of elements in the map and the sum of the lengths of all the byte-strings are, at most, 2\*\*32-1. All the byte strings must have lengths that are a multiple of four.
All values are encoded in little-endian form because every platform that Google software runs on is little-endian.
The header of a message looks like:
```
uint32 num_tags
uint32 offsets[max(0, num_tags-1)]
uint32 tags[num_tags]
```
Following the header are the contents of the byte-strings. The first byte after the header is considered be offset zero and is the start of the value for the first tag. The `offsets` array gives the offset of the value for all tags after the first.
Tags must be in strictly ascending order and offsets must be a multiple of four. Parsers must require this.
Here are some example messages, given in hex:
The empty message consists of four, zero bytes:
```
00000000 # num_tags = 0
```
A message with a single tag (0x1020304) with value 80808080:
```
01000000 # num_tags = 1
# No offsets
04030201 # tag 0x1020304
80808080 # value starts immediately after the header
```
A message with two tags:
```
02000000 # num_tags = 2
04000000 # offset 4 for the value of the second tag
05030200 # tag 0x020305
04030201 # tag 0x1020304
00000000 # value for 0x020305 starts immediately after the header
80808080 # value for 0x1020304 starts at offset four
```
Note that the ordering of the tags is based on their numerical order, not on the lexicographic order of their encodings.
When processing messages, unknown tags are ignored.
## Tag values
Tags can be arbitrary 32-bit values but in this document they will often be written as three or four capital letters, e.g. `NONC`. In these cases, the numeric value of the tag is such that the little-endian encoding of it causes that string to be written out. So the numeric value of `NONC` is 0x434e4f4e. For three-letter tags, the last byte will be given explicitly, e.g. `SIG\x00`.
## Requests
A Roughtime request is a message with at least the tag `NONC`. The value of `NONC` is 64, arbitrary bytes that form a nonce in the protocol. Other tags must be ignored.
(The simplest clients can just generate random values for the nonce. Clients that participate in the [wider ecosystem](/ECOSYSTEM.md) generate nonces in a different way.)
Request messages sent over UDP must be at least 1024 bytes long in order to ensure that a Roughtime server cannot act as an amplifier. The canonical way to ensure this is to include a `PAD\xff` tag, whose value is an arbitrary number of zero bytes. It's expected that a UDP request contain exactly 1024 bytes as there's no point padding the request to more than the minimum.
## Responses
Responses are messages that contain at least the following tags:
* `SREP`: signed response bytes, the value of which is itself a message.
* `SIG\x00`: the signature, an opaque byte-string that authenticates the value of `SREP`. At the moment, Ed25519 is the only supported signature algorithm and so this will be a 64-byte value.
* `CERT`: the server's certificate, which contains a time-limited online key authenticated by the server's long-term key.
* `INDX` and `PATH`: the position and upwards path for this response in a Merkle tree. See the section on signatures, below.
### The signed response and Roughtime UTC.
The signed portion of a response is a message that contains the timestamp from the server as well as the root of a Merkle tree for authenticating it. It's contained in the value of the `SREP` tag.
The timestamp is expressed in two tags: `MIDP` and `RADI`. The `MIDP` tag contains a uint64 which is the midpoint, in microseconds, of a span of time, while the `RADI` tag contains the radius of that span in microseconds, expressed as a uint32. The server asserts that the true time, at the point of processing, lies within that span.
The “true time” for Roughtime is defined as being UTC with a 24-hour linear leap-second smear. That is, when a leap-second is added or removed from UTC it is smeared out over the course of a day. So UTC noon to noon on the date in question will consist of 86,399 or 86,401 SI seconds, with all the smeared seconds being the same length.
As noted, the signed response message also includes the root of a Merkle tree in a `ROOT` tag. The semantics of this are detailed in the next section.
### Authenticating replies
In order to authenticate the response and ensure freshness, the nonce provided in a request must be bound into the signed response. In order to allow a server to sign a batch of responses the nonces are built into a tree and only the root of the tree is included in the signed message.
A signature of the encoded, signed response message is included as the value of the top-level `SIG\x00` tag. This signature must be checked with respect to the online key that's included in the certificate. (See next section.)
Additionally, a client must ensure that their nonce is included the tree. To do so, the values of the `INDX` and `PATH` tags from the top-level response are needed, as well as the value of the `ROOT` tag from the signed response message.
The value of the `INDX` tag is a uint32 and specifies the position of the client's nonce in the tree. The value of the `PATH` tag is a series of hashes on the path from the client's nonce to the root of the tree. The value of the `ROOT` tag is the claimed root of the tree. The hash function used throughout is SHA-512 so the length of the root is 64 bytes and the length of the path is a multiple of 64 bytes.
A client can verify that its nonce is included in a tree using the following pseudo code:
```
index = top-level-response.getU32("INDX")
path = top-level-response.getMultipleOf64Bytes("PATH")
hash = hashLeaf(nonce-from-request)
while len(path) > 0 {
if index&1 == 0 {
hash = hashNode(hash, path[:64])
} else {
hash = hashNode(path[:64], hash)
}
index >>= 1
path = path[64:]
}
if hash != signed-response-message.Get64Bytes("ROOT") {
return error;
}
function hashLeaf(leaf) {
return SHA-512("\x00" + leaf)
}
function hashNode(left, right) {
return SHA-512("\x01" + left + right)
}
```
### Certificates
In order to allow a server to keep its long-term identity key offline, a response includes a certificate’, which is a limited delegation from the long-term key to an online key. This certificate is contained in a message which is the value of the `CERT` tag in a response.
The certificate message contains two tags: `SIG\x00` and `DELE`. The value of the `SIG\x00` tag is a signature of the value of the `DELE` tag, made by the server's long-term key. (Clients must know the server's public key a priori.) Since Ed25519 is currently the only supported signature algorithm, this value will be 64 bytes long.
The contents of the value of the `DELE` tag are a message containing:
* `PUBK`: the online public key. This key is used to produce the signature of the signed response message in the top-level of the response.
* `MINT` and `MAXT`: these uint64s limit the times that the online key can authenticate. The midpoint of any time span using that key must be greater than (or equal to) the value of `MINT` and less than (or equal to) the value of `MAXT`.
### Processing a response
To summarise the above, the full structure of a response (when considering nested messages) looks like this:
* `SREP`
* `ROOT`
* `MIDP`
* `RADI`
* `SIG\x00`
* `INDX`
* `PATH`
* `CERT`
* `SIG\x00`
* `DELE`
* `MINT`
* `MAXT`
* `PUBK`
The procedure to fully process a response results in an authenticated midpoint and radius and contains roughly these steps:
1. Verify the signature in the certificate of the delegation message.
1. Verify the top-level signature of the signed response message using the public key from the delegation.
1. Verify that the nonce from the request is included in the Merkle tree.
1. Verify that the midpoint is within the valid bounds of the delegation.
1. Return the midpoint and radius.