Correctly report midpoint time in response; numerous javadocs and comments

This commit is contained in:
Stuart Stock
2017-01-31 11:08:52 -06:00
parent c62a3e08b2
commit 36cd5d9338
10 changed files with 145 additions and 27 deletions

View File

@ -1,5 +1,5 @@
# Nearenough
**Nearenough** is a Netty-based implementation of the
**Nearenough** is a Java implementation of the
[Roughtime](https://roughtime.googlesource.com/roughtime) secure time synchronization protocol.
[Roughtime](https://roughtime.googlesource.com/roughtime) is a protocol that aims to achieve rough
@ -17,8 +17,7 @@ Nearenough bundles all required dependencies in the `lib` directory. Add those `
your IDE's project classpath. Building is IDE-only for the moment.
## Implementation Status
Nearenough is in its infancy and very little is implemented. Expect significant changes as the code
evolves.
Nearenough is in its infancy. Expect significant changes as the code evolves.
## Contributors
* Stuart Stock, original author (stuart {at} int08h.com)
@ -26,9 +25,9 @@ evolves.
## Copyright and License
Nearenough is Copyright (c) 2017 int08h LLC. All rights reserved.
int08h LLC licenses this file to you under the Apache License, version 2.0 (the "License"); you
may not use this file except in compliance with the License. You may obtain a copy of the License
from the [LICENSE](../master/LICENSE) file included with the software or at:
int08h LLC licenses Nearenough (the "Software") to you under the Apache License, version 2.0
(the "License"); you may not use this Software except in compliance with the License. You may obtain
a copy of the License from the [LICENSE](../master/LICENSE) file included with the Software or at:
http://www.apache.org/licenses/LICENSE-2.0

View File

@ -93,7 +93,7 @@ public final class RtEd25519 {
* @param publicKeyBytes A byte[32] containing the Ed25519 public key to use for verifications.
*/
public Verifier(byte[] publicKeyBytes) throws InvalidKeyException, SignatureException {
checkArgument(publicKeyBytes.length == PUBKEY_LENGTH, "public key incorrect size");
checkArgument(publicKeyBytes.length == PUBKEY_LENGTH, "incorrect public key size");
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyBytes, ED25519_SPEC);
PublicKey publicKey = new EdDSAPublicKey(publicSpec);

View File

@ -15,8 +15,9 @@ import static nearenough.util.Preconditions.checkNotNull;
/**
* An immutable Roughtime protocol message.
* <p>
* Roughtime messages are a map of uint32's to arbitrary byte-strings. See the
* <a href="https://roughtime.googlesource.com/roughtime">Roughtime project site</a> for
* Roughtime messages are a map of uint32's to arbitrary byte-strings.
*
* @see <a href="https://roughtime.googlesource.com/roughtime">Roughtime project site</a> for
* the specification and more details.
*/
public final class RtMessage {
@ -90,7 +91,7 @@ public final class RtMessage {
private int extractNumTags(ByteBuf msg) {
long readNumTags = msg.readUnsignedIntLE();
// Spec says max # tags can be 2^32-1, but capping at 64k tags for the moment.
// Spec says max # tags can be 2^32-1, but capping at 64k tags in this implementation
if (readNumTags < 0 || readNumTags > 0xffff) {
throw new InvalidNumTagsException("invalid num_tags value " + readNumTags);
}

View File

@ -40,7 +40,7 @@ public final class RtMessageBuilder {
checkNotNull(tag, "tag must be non-null");
checkNotNull(msg, "msg must be non-null");
ByteBuf encoded = RtEncoding.toWire(msg, allocator);
ByteBuf encoded = RtWire.toWire(msg, allocator);
return add(tag, encoded);
}
@ -59,7 +59,7 @@ public final class RtMessageBuilder {
public RtMessage build() {
checkArgument(!map.isEmpty(), "Cannot build an empty RtMessage");
int encodedSize = RtEncoding.computeEncodedSize(map);
int encodedSize = RtWire.computeEncodedSize(map);
if (encodedSize < MIN_REQUEST_LENGTH && shouldAddPadding) {
// Additional bytes added to message size for PAD tag (4) and its offset field (4)

View File

@ -32,7 +32,7 @@ public enum RtTag {
// Primitive collection eliminates boxing in fromUnsignedInt which is a hot method
private static final IntObjectMap<RtTag> ENCODING_TO_TAG = new IntObjectHashMap<>(values().length);
// Tags for which values are nested messages
// Tags for which values are themselves RtMessages
private static final Set<RtTag> NESTED_TAGS = EnumSet.of(CERT, DELE, SREP);
static {
@ -77,14 +77,23 @@ public enum RtTag {
this.valueLE = Integer.reverseBytes(wireEncoding);
}
/**
* @return The on-the-wire representation of this tag.
*/
public int wireEncoding() {
return wireEncoding;
}
/**
* @return The little-endian representation of this tag.
*/
public int valueLE() {
return valueLE;
}
/**
* @return True if this tag is numerically less than {@code other}, false otherwise.
*/
public boolean isLessThan(RtTag other) {
checkNotNull(other, "cannot compare to null RtTag");
// Enforcement of the "tags in strictly increasing order" rule is done using the
@ -93,6 +102,9 @@ public enum RtTag {
return Integer.compareUnsigned(valueLE, other.valueLE) < 0;
}
/**
* @return True if the value of this tag is another {@link RtMessage}, false otherwise.
*/
public boolean isNested() {
return NESTED_TAGS.contains(this);
}

View File

@ -2,20 +2,40 @@ package nearenough.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import nearenough.util.BytesUtil;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.Map;
import static nearenough.util.Preconditions.checkNotNull;
import static nearenough.util.Preconditions.checkState;
import static nearenough.protocol.RtConstants.TIMESTAMP_LENGTH;
import static nearenough.util.Preconditions.*;
/**
* Encodes {@link RtMessage Roughtime messages} to their canonical on-the-wire format.
* Encodes/decodes {@link RtMessage Roughtime messages} and fields to/from their on-the-wire format.
*/
public final class RtEncoding {
public final class RtWire {
/**
* Encode the given message using the system default ByteBuf allocator.
* Convert the on-the-wire UTC midpoint value to a {@link ZonedDateTime} in the system default
* time zone.
*
* @param midpBytes The MIDP value
*
* @return A {@link ZonedDateTime} that corresponds to the UTC time from the provided MIDP.
*/
public static ZonedDateTime timeFromMidpoint(byte[] midpBytes) {
checkArgument(midpBytes.length == TIMESTAMP_LENGTH, "invalid MIDP length %s", midpBytes.length);
long midp = BytesUtil.getLongLE(midpBytes, 0);
Instant midpInst = Instant.ofEpochMilli(midp / 1000);
return ZonedDateTime.ofInstant(midpInst, ZoneId.systemDefault());
}
/**
* Encode the given message for network transmission using the system default ByteBuf allocator.
*
* @return A {@link ByteBuf} containing this message encoded for transmission.
*/
@ -24,7 +44,7 @@ public final class RtEncoding {
}
/**
* Encode the given message using the provided ByteBuf allocator.
* Encode the given message for network transmission using the provided ByteBuf allocator.
*
* @return A {@link ByteBuf} containing this message encoded for transmission.
*/
@ -47,6 +67,9 @@ public final class RtEncoding {
return buf;
}
/**
* @return The size in bytes of the on-the-wire encoding of the provided {@code map}.
*/
/*package*/ static int computeEncodedSize(Map<RtTag, byte[]> map) {
int numTagsSum = 4;
int tagsSum = 4 * map.size();

View File

@ -0,0 +1,43 @@
package nearenough.util;
/**
* Utility methods for working with {@code byte[]}. Extracted from Netty.
*/
public final class BytesUtil {
public static int getInt(byte[] memory, int index) {
return (memory[index] & 0xff) << 24 |
(memory[index + 1] & 0xff) << 16 |
(memory[index + 2] & 0xff) << 8 |
memory[index + 3] & 0xff;
}
public static int getIntLE(byte[] memory, int index) {
return memory[index] & 0xff |
(memory[index + 1] & 0xff) << 8 |
(memory[index + 2] & 0xff) << 16 |
(memory[index + 3] & 0xff) << 24;
}
public static long getLong(byte[] memory, int index) {
return ((long) memory[index] & 0xff) << 56 |
((long) memory[index + 1] & 0xff) << 48 |
((long) memory[index + 2] & 0xff) << 40 |
((long) memory[index + 3] & 0xff) << 32 |
((long) memory[index + 4] & 0xff) << 24 |
((long) memory[index + 5] & 0xff) << 16 |
((long) memory[index + 6] & 0xff) << 8 |
(long) memory[index + 7] & 0xff;
}
public static long getLongLE(byte[] memory, int index) {
return (long) memory[index] & 0xff |
((long) memory[index + 1] & 0xff) << 8 |
((long) memory[index + 2] & 0xff) << 16 |
((long) memory[index + 3] & 0xff) << 24 |
((long) memory[index + 4] & 0xff) << 32 |
((long) memory[index + 5] & 0xff) << 40 |
((long) memory[index + 6] & 0xff) << 48 |
((long) memory[index + 7] & 0xff) << 56;
}
}

View File

@ -0,0 +1,25 @@
package nearenough.util;
import java.time.Instant;
/**
* Simplistic accounting of UTC leap-seconds to work around lack of leap-second support in
* Java's native date/time libraries.
* <p>
* Java maintains its timekeeping using a time-scale known as the <em>Java Time-Scale</em>. This
* time-scale divides all days into exactly 86,400 seconds, thus is ignorant of leap-seconds. See
* {@link Instant} for the gory details.
* <p>
* Roughtime, being leap-second aware, necessitates a way to account for UTC leap-seconds. Herein
* we cheat and simply track the number of accumulated leap seconds since 1972.
* <p>
* An alternate way to do this would be to rely on something like the excellent
* <a href="http://www.threeten.org/threeten-extra/">ThreeTen-Extra</a> library and its
* <a href="http://www.threeten.org/threeten-extra/apidocs/org/threeten/extra/scale/UtcInstant.html">
* UtcInstant</a>.
*/
public final class LeapSeconds {
/** The number of leap seconds that have occurred in UTC as-of 2017-01-01 */
public static final int NUM_LEAP_SECONDS_2017 = 28;
}

View File

@ -15,8 +15,11 @@ import java.net.InetSocketAddress;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.time.ZonedDateTime;
import java.util.Random;
import static nearenough.protocol.RtConstants.TIMESTAMP_LENGTH;
import static nearenough.util.Preconditions.checkState;
import static net.i2p.crypto.eddsa.Utils.hexToBytes;
/**
@ -56,7 +59,7 @@ public final class ResponseDumper {
.add(RtTag.NONC, nonce)
.build();
ByteBuf buf = RtEncoding.toWire(msg);
ByteBuf buf = RtWire.toWire(msg);
ctx.writeAndFlush(new DatagramPacket(buf, addr))
.addListener(fut -> {
@ -78,9 +81,21 @@ public final class ResponseDumper {
verifyCertSignature(response);
verifySrepSignature(response);
printTime(response);
ctx.close();
}
private void printTime(RtMessage response) {
ZonedDateTime now = ZonedDateTime.now();
RtMessage srepMsg = RtMessage.fromBytes(response.get(RtTag.SREP));
byte[] midpBytes = srepMsg.get(RtTag.MIDP);
checkState(midpBytes.length == TIMESTAMP_LENGTH);
System.out.println("Midp : " + RtWire.timeFromMidpoint(midpBytes));
System.out.println("Now : " + now);
}
private void verifySrepSignature(RtMessage response) throws InvalidKeyException, SignatureException {
RtMessage certMsg = RtMessage.fromBytes(response.get(RtTag.CERT));
RtMessage deleMsg = RtMessage.fromBytes(certMsg.get(RtTag.DELE));
@ -103,7 +118,7 @@ public final class ResponseDumper {
verifier.update(certMsg.get(RtTag.DELE));
byte[] certSig = certMsg.get(RtTag.SIG);
assert certSig.length == 64 : "bad length";
checkState(certSig.length == 64, "invalid signature length %s", certSig.length);
System.out.println("CERT signature is " + verifier.verify(certSig));
}

View File

@ -16,7 +16,7 @@ public final class MessageEncodingTest {
@Test
public void sizeOfEmptyMessage() {
int size = RtEncoding.computeEncodedSize(Collections.emptyMap());
int size = RtWire.computeEncodedSize(Collections.emptyMap());
// Empty message is 4 bytes, a single num_tags value
assertThat(size, equalTo(4));
}
@ -24,7 +24,7 @@ public final class MessageEncodingTest {
@Test
public void sizeOfSingleTagMessage() {
Map<RtTag, byte[]> map = Collections.singletonMap(RtTag.NONC, new byte[]{1, 2, 3, 4});
int size = RtEncoding.computeEncodedSize(map);
int size = RtWire.computeEncodedSize(map);
// Single tag message is 4 (num_tags) + 4 (NONC) + 4 (value)
assertThat(size, equalTo(12));
}
@ -35,7 +35,7 @@ public final class MessageEncodingTest {
map.put(RtTag.NONC, new byte[4]);
map.put(RtTag.PAD, new byte[4]);
int size = RtEncoding.computeEncodedSize(map);
int size = RtWire.computeEncodedSize(map);
// Two tag message
// 4 num_tags
// 8 (NONC, PAD) tags
@ -47,7 +47,7 @@ public final class MessageEncodingTest {
@Test
public void encodeEmptyMessage() {
RtMessage msg = RtMessage.fromBytes(new byte[]{0, 0, 0, 0});
ByteBuf onWire = RtEncoding.toWire(msg);
ByteBuf onWire = RtWire.toWire(msg);
// Empty message will be a single uint32
assertThat(onWire.readableBytes(), equalTo(4));
@ -64,7 +64,7 @@ public final class MessageEncodingTest {
.build();
// Wire encoding is 4 (num_tags) + 4 (CERT) + 64 (CERT value)
ByteBuf onWire = RtEncoding.toWire(msg);
ByteBuf onWire = RtWire.toWire(msg);
assertThat(onWire.readableBytes(), equalTo(72));
// num_tags
@ -101,7 +101,7 @@ public final class MessageEncodingTest {
// 4 MAXT offset
// 24 INDX value
// 32 MAXT value
ByteBuf onWire = RtEncoding.toWire(msg);
ByteBuf onWire = RtWire.toWire(msg);
assertThat(onWire.readableBytes(), equalTo(4 + 8 + 4 + 24 + 32));
// num_tags