Correctly report midpoint time in response; numerous javadocs and comments
This commit is contained in:
11
README.md
11
README.md
@ -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
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
43
src/nearenough/util/BytesUtil.java
Normal file
43
src/nearenough/util/BytesUtil.java
Normal 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;
|
||||
}
|
||||
}
|
25
src/nearenough/util/LeapSeconds.java
Normal file
25
src/nearenough/util/LeapSeconds.java
Normal 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;
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user