add junit test cases

This commit is contained in:
Harald Hoyer 2016-09-12 16:46:40 +02:00
parent 628cf77b9f
commit 6b50ca4921
10 changed files with 796 additions and 59 deletions

171
README.md
View file

@ -1,10 +1,179 @@
scratch repo for some ideas
### How to start:
```bash
$ git clone https://github.com/haraldh/opretj.git
$ cd opretj
$ mvn install
$ mvn package
$ cd opret-testapp
$ mvn exec:java -Dexec.args='--net=TEST'
```
# Key Management on the Blockchain
The blockchain as a distributed immutable ledger is a good tool to use as a public key infrastructure (PKI). On this PKI, we can announce new public keys, sign them and revoke them. Everybody can scan the blockchain for key related announcements and nobody can remove or falsify those afterwards, without notice.
For these key announcements the same properties apply as for the currency. That means an attacker will have to cut those who want new information from the blockchain completely off from the distributed network. By sending the block headers on different media (like satellite, radio or TV), a blockchain receiver can quickly see, that one of his sources diverges from the others and a warning signal can be issued, that something fishy is going on.
For this implementation of PKI on the blockchain, the bitcoin blockchain is chosen, because it is backed up by enough money and miners to ensure the integrity and immutable nature. An alternative blockchain PKI could be implemented on ethereum, which has more powerful interfaces to implement a PKI.
## Restrictions on the bitcoin blockchain
Arbitrary data can be stored in every bitcoin transaction by using a transaction output script, which begins with OP_RETURN. OP_RETURN can be followed by data chunks. There can only be one OP_RETURN transaction output script in a transaction. The size of the script including OP_RETURN must not exceed 82 bytes. Every data chunk has it's length prepended. For sizes from 1-75 bytes, the size field only consumes one byte.
An example OP_RETURN script with a 32 byte and 16 byte data chunk looks like this:
OP_RETURN 32 [32 bytes data chunk] 16 [16 byte data chunk]
which results in a script length of 51 bytes.
## Bitcoin blockchain for thin clients
Downloading the full bitcoin blockchain requires network bandwidth and storage. Mobile clients therefore use the SPV protocol to get filtered blockchain data from full nodes. For further reading consult the links in the [bitcoin glossary on SPV] (https://bitcoin.org/en/glossary/simplified-payment-verification).
Every data chunk in the OP_RETURN script can be used as a bloom filter element for thin clients. That means, that the PKI key announcements should have on data chunk, which a thin client can use for the filter to save bandwidth.
## PKI
Because of the limited amount of data, which can be stored, this implementation of a PKI on the bitcoin blockchain uses elliptic curve keys and for the ease of implementation curve 25519 and the [libsodium] (https://download.libsodium.org/doc/) functions.
## Mode of operation
* user creates 256bit master key (MK) with Ed25519 ECC curve 25519
* user creates derived 256bit key from master key as signing key 1 (K1)
* K1 public key (K1PK) is announced on the blockchain with 0xECA1 and 0xECA2
* user creates derived 256bit key from master key as signing key 2 (K2)
* K2 public key (K2PK) is announced as next key of K1 with 0xECA3 and 0xECA4
* K2 key and MK are removed from device
* K1 secret key is used on device to sign documents and ephemeral encryption keys
* K1 revocation record (K1RR) is stored somewhere for later publication
* If K1SK lost or breached:
+ K1 is revoked on the blockchain with 0xEC0F
+ MK is used to calculate K3
+ K3 is announced as next key of K2 with 0xECA3 and 0xECA4
+ K3 and MK are removed from device
A MK is stored along with the key birthday, which is the date of the first appearance on the blockchain.
## PKI blockchain announcements in Detail
### MVK announce subkey VK 0xECA[1,2] - A-nnounce
nonce[0:32] = nonce[0:16] | nonce[16:32]
data chunks are prepended with zeros, if its length is smaller than 16.
E.g.
nonce[0:16] = 0x1F -> 0x0000000000000000000000000000001F
nonce[16:32] = 0x2F -> 0x0000000000000000000000000000002F
nonce[0:32] = 0x0000000000000000000000000000001F0000000000000000000000000000002F
If nonce is missing completely, then
nonce[0:32] = 0x0000000000000000000000000000000000000000000000000000000000000000
is assumed.
A nonce **must** be used only once. Either only one VK_pub is announced per MVK ever and nonce is missing,
or for every MVK announcement, the nonce has to be *unique* or *true random* bytes.
sharedkey = sha256(sha256(MVK_pub | nonce))
xornonce[24] = sha256(sharedkey | nonce)[0:24]
sig[64] = crypto_sign(VK_pub, MKV)
msg[96] = VK_pub || sig
cipher[96] = crypto_stream_xor(msg, xornonce, sharedkey)
clients may flush T1, if T2 does not follow in the next 20 blocks
clients may flush T2, if T1 does not follow in the next 20 blocks
| | OP | Chunk1 | Chunk2 | Chunk3 |
|:-----|:---------:|:------:|:---------------------------:|:-------------------------:|
| T1 | OP_RETURN | 0xECA1 | cipher[00:48] + data[0:16] | 12 Byte sha256(MVK)[0:12] |
| Size | 1 | 3 | 49 | 13 |
| T2 | OP_RETURN | 0xECA2 | cipher[48:96] + data[16:32] | 12 Byte sha256(MVK)[0:12] |
| Size | 1 | 3 | 49 | 13 |
### MVK announce next subkey VK_n+1 0xECA[3,4] - A-nnounce
sharedkey = sha256(sha256(VK_n_pub))
nonce[24] = sha256(sharedkey)[0:24]
sig[64] = crypto_sign(VK_n+1_pub, MKV)
msg[96] = VK_n+1_pub || sig
cipher[96] = crypto_stream_xor(msg, nonce, sharedkey)
clients may flush T1, if T2 does not follow in the next 20 blocks
clients may flush T2, if T1 does not follow in the next 20 blocks
| | OP | Chunk1 | Chunk2 | Chunk3 | Chunk4 |
|:-----|:---------:|:------:|:--------------:|:-------------------------:|:--------------------------:|
| T1 | OP_RETURN | 0xECA3 | cipher[00:48] | 12 Byte sha256(MVK)[0:12] | 12 Byte sha256(VK_n)[0:12] |
| Size | 1 | 3 | 49 | 13 | 13 |
| T2 | OP_RETURN | 0xECA4 | cipher[48:96] | 12 Byte sha256(MVK)[0:12] | 12 Byte sha256(VK_n)[0:12] |
| Size | 1 | 3 | 49 | 13 | 13 |
### Public Doc or other key OK sign 0xEC5[1,2]
sign[64] = Sign_Key('Sign ' || sha256(Doc/OK))
data = optional data (max 2*19 bytes)
clients may flush T1, if T2 does not follow in the next 20 blocks
clients may flush T2, if T1 does not follow in the next 20 blocks
| | OP | Chunk1 | Chunk2 | Chunk3 | Chunk4 |
|:-----|:---------:|:------:|:------------------:|:-------------------------:|:----------------------------:|
| T1 | OP_RETURN | 0xEC51 | sign[00:32] + data | 12 Byte sha256(Key)[0:12] | 12 Byte sha256(Doc/OK)[0:12] |
| Size | 1 | 3 | 33 | 13 | 13 |
| T2 | OP_RETURN | 0xEC52 | sign[32:64] + data | 12 Byte sha256(Key)[0:12] | 12 Byte sha256(Doc/OK)[0:12] |
| Size | 1 | 3 | 33 | 13 | 13 |
### Revoke a Key 0xEC0F - 0FF
OP_RETURN 0xEC0F Sign('Revoke ' || sha256(Key)) 64 Bytes + 12 Byte sha256(Key)
### Anonymous Doc/VK sign OxEC1D - ID
Proof, that Key could sign something at that date.
OP_RETURN OxEC1D Sign('Sign ' || sha256(Doc/VK)) 64 Bytes + 12 Byte sha256(Doc/VK)[0:12]
### Doc Proof of Existence 0xEC1C - I see
Proof, that a document existed at that point of time.
OP_RETURN 0xEC1C 32 Byte sha256(Doc)
~~### Key note 0xEC10
annotate a <note> for a Key encrypted with the encryption key EK
OP_RETURN 0xEC10 || Box_ENC(<note>) 64 Bytes || 12 Byte sha256(EK_pub || Key)[0:12]~~
## Example on the Bitcoin Blockchain
An example transaction with a key revocation can be seen on the bitcoin blockchain as transaction [c7457b452c41deea0f2a34ef8bf7596c758002714062e869516b6dd5602b5565](https://www.blocktrail.com/BTC/tx/c7457b452c41deea0f2a34ef8bf7596c758002714062e869516b6dd5602b5565#tx_messages).
In this transaction a VK fb2e360caf811b3aaf534d0458c2a2ca3e1f213b244a6f83af1ab50eddacdd8c is revoked as seen with 0xEC0F
The sha256sum of the PK is f5105e87388c219e43ad9a9856c50df9f9b4a0e87a8bd32d0f72534d83a2df74
```
$ echo fb2e360caf811b3aaf534d0458c2a2ca3e1f213b244a6f83af1ab50eddacdd8c | xxd -r -p | sha256sum
f5105e87388c219e43ad9a9856c50df9f9b4a0e87a8bd32d0f72534d83a2df74
```
The message to verify is 'Revoke ' followed by the hash of VK:
```
5265766f6b6520 f5105e87388c219e43ad9a9856c50df9f9b4a0e87a8bd32d0f72534d83a2df74
```
and the corresponding signature as seen on the blockchain:
```
34dccafe91cb0b2b30175ead0eacc1481ee7428da70158035ab657914634801a37056bbf88e27058303e6f9e6cd38d1704a62b54ec9723614e6c1cf04b052e0f
```
With pysodium, we can check the signature quickly:
```
from pysodium import *
import binascii
if __name__ == '__main__':
pk = binascii.unhexlify(b'fb2e360caf811b3aaf534d0458c2a2ca3e1f213b244a6f83af1ab50eddacdd8c')
msg = b'Revoke ' + binascii.unhexlify(b'f5105e87388c219e43ad9a9856c50df9f9b4a0e87a8bd32d0f72534d83a2df74')
sig = binascii.unhexlify(b'34dccafe91cb0b2b30175ead0eacc1481ee7428da70158035ab657914634801a37056bbf88e27058303e6f9e6cd38d1704a62b54ec9723614e6c1cf04b052e0f')
try:
crypto_sign_verify_detached(sig, msg, pk)
print "Signature OK"
except ValueError:
print "sig does not match"
```
and of course it matches.

View file

@ -197,7 +197,7 @@ public class App {
earliestTime = Utils.currentTimeSeconds();
}
bs.addVerifyKey(SK.getVerifyKey(), earliestTime);
bs.addVerifyKey(SK.getMasterVerifyKey(), earliestTime);
final OPRETWalletAppKit kit = new OPRETWalletAppKit(params, new File("."), "opretwallet" + params.getId(), bs);

View file

@ -1,3 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/test/java=UTF-8
encoding/<project>=UTF-8

View file

@ -7,16 +7,16 @@
<version>0.0.2-SNAPSHOT</version>
</parent>
<artifactId>opretj</artifactId>
<repositories>
<repository>
<id>oss-sonatype</id>
<name>oss-sonatype</name>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<repositories>
<repository>
<id>oss-sonatype</id>
<name>oss-sonatype</name>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
@ -30,7 +30,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<version>4.11</version>
<scope>test</scope>
</dependency>
@ -64,6 +64,15 @@
<target>1.8</target>
</configuration>
</plugin>
<!-- Unit tests plugin, to skip runing test add -Dmaven.test.skip -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
@ -76,4 +85,3 @@
<url>https://github.com/haraldh/opretj/issues</url>
</issueManagement>
</project>
</project>

View file

@ -27,6 +27,10 @@ public class MasterSigningKey extends SigningKey {
this.keyindex = new ArrayList<>(keyindex);
}
public MasterVerifyKey getMasterVerifyKey() {
return new MasterVerifyKey(this.getVerifyKey().toBytes());
}
public MasterSigningKey getNextValidSubKey(final Long offset) {
return getSubKey(subkeyindex + offset);
}

View file

@ -3,6 +3,8 @@ package org.tcpid.key;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import org.tcpid.opretj.OPRETTransaction;
public class MasterVerifyKey extends VerifyKey {
private final LinkedList<VerifyKey> subkeys = new LinkedList<>();
@ -29,7 +31,7 @@ public class MasterVerifyKey extends VerifyKey {
subkeys.remove(i);
}
public void setFirstValidSubKey(final VerifyKey key) {
public void setFirstValidSubKey(final VerifyKey key, final OPRETTransaction t1, final OPRETTransaction t2) {
if (!subkeys.isEmpty()) {
throw new IndexOutOfBoundsException("Subkey list is not empty");
}

View file

@ -1,7 +1,7 @@
package org.tcpid.opretj;
import org.tcpid.key.VerifyKey;
import org.tcpid.key.MasterVerifyKey;
public interface OPRETECEventListener {
void onOPRETRevoke(VerifyKey key);
void onOPRETRevoke(MasterVerifyKey key);
}

View file

@ -1,6 +1,7 @@
package org.tcpid.opretj;
import static org.bitcoinj.script.ScriptOpCodes.OP_RETURN;
import static org.libsodium.jni.NaCl.sodium;
import static org.libsodium.jni.SodiumConstants.NONCE_BYTES;
import static org.libsodium.jni.SodiumConstants.SECRETKEY_BYTES;
@ -20,12 +21,15 @@ import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.ListenerRegistration;
import org.bitcoinj.utils.Threading;
import org.libsodium.jni.Sodium;
import org.libsodium.jni.crypto.Hash;
import org.libsodium.jni.crypto.Util;
import org.libsodium.jni.encoders.Encoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tcpid.key.HMACSHA512256;
import org.tcpid.key.MasterSigningKey;
import org.tcpid.key.MasterVerifyKey;
import org.tcpid.key.SigningKey;
import org.tcpid.key.VerifyKey;
@ -35,7 +39,15 @@ public class OPRETECParser extends OPRETBaseHandler {
private static final Logger logger = LoggerFactory.getLogger(OPRETECParser.class);
public static final Hash HASH = new Hash();
private static final List<Byte> OPRET_MAGIC = Bytes.asList(Utils.HEX.decode("ec0f"));
private static final List<Byte> OPRET_MAGIC_EC1C = Bytes.asList(Utils.HEX.decode("ec1c"));
private static final List<Byte> OPRET_MAGIC_EC1D = Bytes.asList(Utils.HEX.decode("ec1d"));
private static final List<Byte> OPRET_MAGIC_ECA1 = Bytes.asList(Utils.HEX.decode("eca1"));
private static final List<Byte> OPRET_MAGIC_ECA2 = Bytes.asList(Utils.HEX.decode("eca2"));
private static final List<Byte> OPRET_MAGIC_ECA3 = Bytes.asList(Utils.HEX.decode("eca3"));
private static final List<Byte> OPRET_MAGIC_ECA4 = Bytes.asList(Utils.HEX.decode("eca4"));
private static final List<Byte> OPRET_MAGIC_EC51 = Bytes.asList(Utils.HEX.decode("ec51"));
private static final List<Byte> OPRET_MAGIC_EC52 = Bytes.asList(Utils.HEX.decode("ec52"));
private static final List<Byte> OPRET_MAGIC_EC0F = Bytes.asList(Utils.HEX.decode("ec0f"));
public static boolean checkKeyforRevoke(final VerifyKey k, final byte[] sig) {
logger.debug("CHECKING REVOKE PKHASH {} - SIG {}", Utils.HEX.encode(k.toHash()), Utils.HEX.encode(sig));
@ -65,8 +77,20 @@ public class OPRETECParser extends OPRETBaseHandler {
protected final Map<Sha256Hash, PartialMerkleTree> merkleHashMap = Collections.synchronizedMap(new HashMap<>());
protected final Map<Sha256Hash, OPRETTransaction> transHashMap = Collections.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<OPRETTransaction>> transA1HashMap = Collections
.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<OPRETTransaction>> transA2HashMap = Collections
.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<OPRETTransaction>> transA3HashMap = Collections
.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<OPRETTransaction>> transA4HashMap = Collections
.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<OPRETTransaction>> trans51HashMap = Collections
.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<OPRETTransaction>> trans52HashMap = Collections
.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<VerifyKey>> verifyKeys = Collections.synchronizedMap(new HashMap<>());
protected final Map<List<Byte>, List<MasterVerifyKey>> verifyKeys = Collections.synchronizedMap(new HashMap<>());
private final CopyOnWriteArrayList<ListenerRegistration<OPRETECEventListener>> opReturnChangeListeners = new CopyOnWriteArrayList<>();
@ -80,10 +104,10 @@ public class OPRETECParser extends OPRETBaseHandler {
opReturnChangeListeners.add(new ListenerRegistration<OPRETECEventListener>(listener, Threading.SAME_THREAD));
}
public void addVerifyKey(final VerifyKey key, final long earliestTime) {
public void addVerifyKey(final MasterVerifyKey key, final long earliestTime) {
final List<Byte> hash = Bytes.asList(key.getShortHash());
if (!verifyKeys.containsKey(hash)) {
verifyKeys.put(hash, new ArrayList<VerifyKey>());
verifyKeys.put(hash, new ArrayList<MasterVerifyKey>());
}
verifyKeys.get(hash).add(key);
@ -91,36 +115,6 @@ public class OPRETECParser extends OPRETBaseHandler {
addOPRET(key.getShortHash(), earliestTime);
}
private boolean checkData(final OPRETTransaction t) {
final List<List<Byte>> opret_data = new ArrayList<>(t.opretData);
logger.debug("checking {}", opret_data);
if (opret_data.size() != 3) {
return false;
}
List<Byte> chunk;
chunk = opret_data.get(0);
if (!chunk.equals(OPRET_MAGIC)) {
logger.debug("chunk 0: != OPRET_MAGIC");
return false;
}
chunk = opret_data.get(1);
if ((chunk.size() != 64)) {
logger.debug("chunk 1 size != 64, but {}", chunk.size());
return false;
}
chunk = opret_data.get(2);
if ((chunk.size() != 12)) {
logger.debug("chunk 2 size!= 12 but {} ", chunk.size());
return false;
}
return handleRevoke(t);
}
public boolean cryptoSelfTest() {
if (NONCE_BYTES > HMACSHA512256.HMACSHA512256_BYTES) {
logger.error("NONCE_BYTES > HMACSHA512256.HMACSHA512256_BYTES: {} > {}", NONCE_BYTES,
@ -169,14 +163,24 @@ public class OPRETECParser extends OPRETBaseHandler {
return true;
}
private boolean handleRevoke(final OPRETTransaction t) {
final List<Byte> pkhash = t.opretData.get(2);
private boolean handleEC0F(final OPRETTransaction t) {
final byte[] sig = Bytes.toArray(t.opretData.get(1));
if ((sig.length != 64)) {
logger.debug("chunk 1 size != 64, but {}", sig.length);
return false;
}
final List<Byte> pkhash = t.opretData.get(2);
if ((pkhash.size() != 12)) {
logger.debug("chunk 2 size!= 12 but {} ", pkhash.size());
return false;
}
if (!verifyKeys.containsKey(pkhash)) {
return false;
}
for (final VerifyKey k : verifyKeys.get(t.opretData.get(2))) {
for (final MasterVerifyKey k : verifyKeys.get(pkhash)) {
if (checkKeyforRevoke(k, sig)) {
if (k.isRevoked()) {
logger.debug("Duplicate REVOKE PK {} - SIG {}", Utils.HEX.encode(k.getShortHash()),
@ -193,12 +197,284 @@ public class OPRETECParser extends OPRETBaseHandler {
return false;
}
@Override
public void pushTransaction(final OPRETTransaction t) {
checkData(t);
private boolean handleEC1C(final OPRETTransaction t) {
// TODO Auto-generated method stub
return false;
}
protected void queueOnOPRETRevoke(final VerifyKey key) {
private boolean handleEC1D(final OPRETTransaction t) {
final byte[] sig = Bytes.toArray(t.opretData.get(1));
if ((sig.length != 64)) {
logger.debug("chunk 1 size != 64, but {}", sig.length);
return false;
}
final List<Byte> pkhash = t.opretData.get(2);
if ((pkhash.size() != 12)) {
logger.debug("chunk 2 size!= 12 but {} ", pkhash.size());
return false;
}
if (!verifyKeys.containsKey(pkhash)) {
return false;
}
for (final MasterVerifyKey k : verifyKeys.get(pkhash)) {
if (checkKeyforRevoke(k, sig)) {
if (k.isRevoked()) {
logger.debug("Duplicate REVOKE PK {} - SIG {}", Utils.HEX.encode(k.getShortHash()),
Utils.HEX.encode(sig));
} else {
k.setRevoked(true);
logger.debug("REVOKE PK {} - SIG {}", Utils.HEX.encode(k.getShortHash()), Utils.HEX.encode(sig));
}
queueOnOPRETId(k);
return true;
}
}
return false;
}
private boolean handleEC51(final OPRETTransaction t) {
// TODO Auto-generated method stub
return false;
}
private boolean handleEC52(final OPRETTransaction t) {
// TODO Auto-generated method stub
return false;
}
private boolean handleECA1(final OPRETTransaction t1) {
// FIXME: refactor with handleECA2
logger.debug("handleECA1");
final byte[] data1 = Bytes.toArray(t1.opretData.get(1));
if (((data1.length < 48) || (data1.length > 64))) {
logger.debug("invalid chunk1 size = {}", data1.length);
return false;
}
final List<Byte> pkhash = t1.opretData.get(2);
if ((pkhash.size() != 12)) {
logger.debug("chunk 2 size != 12 but {} ", pkhash.size());
return false;
}
if (!verifyKeys.containsKey(pkhash)) {
return false;
}
if (transA2HashMap.containsKey(pkhash)) {
for (final OPRETTransaction t2 : transA2HashMap.get(pkhash)) {
final byte[] data2 = Bytes.toArray(t2.opretData.get(1));
final byte[] cipher = Bytes.concat(Arrays.copyOfRange(data1, 0, 48), Arrays.copyOfRange(data2, 0, 48));
BigInteger nonce1 = BigInteger.ZERO;
BigInteger nonce2 = BigInteger.ZERO;
if (data1.length > 48) {
nonce1 = new BigInteger(1, Arrays.copyOfRange(data1, 48, data1.length));
logger.debug("nonce1 {}", Encoder.HEX.encode(nonce1.toByteArray()));
logger.debug("nonce1shift {}", Encoder.HEX.encode(nonce1.shiftLeft(16 * 8).toByteArray()));
}
if (data2.length > 48) {
nonce2 = new BigInteger(1, Arrays.copyOfRange(data2, 48, data2.length));
logger.debug("nonce2 {}", Encoder.HEX.encode(nonce2.toByteArray()));
}
final BigInteger nonce = nonce1.shiftLeft(16 * 8).or(nonce2);
logger.debug("nonceshift {}", Encoder.HEX.encode(nonce.toByteArray()));
byte[] noncebytes = Util.prependZeros(32, nonce.toByteArray());
noncebytes = Arrays.copyOfRange(noncebytes, noncebytes.length - 32, noncebytes.length);
for (final MasterVerifyKey k : verifyKeys.get(pkhash)) {
byte[] sharedkey, xornonce;
sharedkey = HASH.sha256(HASH.sha256(Bytes.concat(k.toBytes(), noncebytes)));
xornonce = Arrays.copyOfRange(HASH.sha256(Bytes.concat(sharedkey, noncebytes)), 0, 24);
logger.debug("checking key {}", Encoder.HEX.encode(k.toBytes()));
logger.debug("noncebytes {}", Encoder.HEX.encode(noncebytes));
logger.debug("noncebytes len {}", noncebytes.length);
logger.debug("xornonce {}", Encoder.HEX.encode(xornonce));
logger.debug("sharedkey {}", Encoder.HEX.encode(sharedkey));
sodium();
final byte[] msg = Util.zeros(96);
Sodium.crypto_stream_xsalsa20_xor(msg, cipher, 96, xornonce, sharedkey);
final byte[] vk = Arrays.copyOfRange(msg, 0, 32);
final byte[] sig = Arrays.copyOfRange(msg, 32, 96);
try {
logger.debug("Checking sig {} with key {}", Encoder.HEX.encode(sig), Encoder.HEX.encode(vk));
k.verify(vk, sig);
} catch (final RuntimeException e) {
logger.debug("sig does not match");
continue;
}
logger.debug("sig matches");
k.setFirstValidSubKey(new MasterVerifyKey(vk), t1, t2);
transA2HashMap.get(pkhash).remove(t2);
if (transA2HashMap.get(pkhash).isEmpty()) {
transA2HashMap.remove(pkhash);
}
return true;
}
}
}
if (!transA1HashMap.containsKey(pkhash)) {
transA1HashMap.put(pkhash, new ArrayList<OPRETTransaction>());
}
transA1HashMap.get(pkhash).add(t1);
return false;
}
private boolean handleECA2(final OPRETTransaction t2) {
// FIXME: refactor with handleECA1
logger.debug("handleECA2");
final byte[] data2 = Bytes.toArray(t2.opretData.get(1));
if (((data2.length < 48) || (data2.length > 64))) {
logger.debug("invalid chunk1 size = {}", data2.length);
return false;
}
final List<Byte> pkhash = t2.opretData.get(2);
if ((pkhash.size() != 12)) {
logger.debug("chunk 2 size != 12 but {} ", pkhash.size());
return false;
}
if (!verifyKeys.containsKey(pkhash)) {
logger.debug("pkash not in hashmap");
return false;
}
if (transA1HashMap.containsKey(pkhash)) {
for (final OPRETTransaction t1 : transA1HashMap.get(pkhash)) {
final byte[] data1 = Bytes.toArray(t1.opretData.get(1));
final byte[] cipher = Bytes.concat(Arrays.copyOfRange(data1, 0, 48), Arrays.copyOfRange(data2, 0, 48));
BigInteger nonce1 = BigInteger.ZERO;
BigInteger nonce2 = BigInteger.ZERO;
if (data1.length > 48) {
nonce1 = new BigInteger(1, Arrays.copyOfRange(data1, 48, data1.length));
}
if (data2.length > 48) {
nonce2 = new BigInteger(1, Arrays.copyOfRange(data2, 48, data2.length));
}
final BigInteger nonce = nonce1.shiftLeft(16 * 8).or(nonce2);
byte[] noncebytes = Util.prependZeros(32, nonce.toByteArray());
noncebytes = Arrays.copyOfRange(noncebytes, noncebytes.length - 32, noncebytes.length);
for (final MasterVerifyKey k : verifyKeys.get(pkhash)) {
byte[] sharedkey, xornonce;
logger.debug("checking key {}", Encoder.HEX.encode(k.toBytes()));
logger.debug("noncebytes {}", Encoder.HEX.encode(noncebytes));
logger.debug("noncebytes len {}", noncebytes.length);
sharedkey = HASH.sha256(HASH.sha256(Bytes.concat(k.toBytes(), noncebytes)));
xornonce = Arrays.copyOfRange(HASH.sha256(Bytes.concat(sharedkey, noncebytes)), 0, 24);
logger.debug("xornonce {}", Encoder.HEX.encode(xornonce));
logger.debug("sharedkey {}", Encoder.HEX.encode(sharedkey));
sodium();
final byte[] msg = Util.zeros(96);
Sodium.crypto_stream_xsalsa20_xor(msg, cipher, 96, xornonce, sharedkey);
final byte[] vk = Arrays.copyOfRange(msg, 0, 32);
final byte[] sig = Arrays.copyOfRange(msg, 32, 96);
try {
logger.debug("Checking sig {} with key {}", Encoder.HEX.encode(sig), Encoder.HEX.encode(vk));
k.verify(vk, sig);
} catch (final RuntimeException e) {
logger.debug("sig does not match");
continue;
}
logger.debug("sig matches");
k.setFirstValidSubKey(new MasterVerifyKey(vk), t1, t2);
transA1HashMap.get(pkhash).remove(t1);
if (transA1HashMap.get(pkhash).isEmpty()) {
transA1HashMap.remove(pkhash);
}
return true;
}
}
}
if (!transA2HashMap.containsKey(pkhash)) {
transA2HashMap.put(pkhash, new ArrayList<OPRETTransaction>());
}
transA2HashMap.get(pkhash).add(t2);
logger.debug("nothing in A1 HashMap");
return false;
}
private boolean handleECA3(final OPRETTransaction t) {
// TODO Auto-generated method stub
return false;
}
private boolean handleECA4(final OPRETTransaction t) {
// TODO Auto-generated method stub
return false;
}
protected boolean handleTransaction(final OPRETTransaction t) {
logger.debug("checking {}", t.opretData);
if ((t.opretData.size() != 2) && (t.opretData.size() != 3) && (t.opretData.size() != 4)) {
return false;
}
final List<Byte> chunk = t.opretData.get(0);
if (chunk.equals(OPRET_MAGIC_EC0F)) {
return handleEC0F(t);
}
if (chunk.equals(OPRET_MAGIC_EC1C)) {
return handleEC1C(t);
}
if (chunk.equals(OPRET_MAGIC_EC1D)) {
return handleEC1D(t);
}
if (chunk.equals(OPRET_MAGIC_ECA1)) {
return handleECA1(t);
}
if (chunk.equals(OPRET_MAGIC_ECA2)) {
return handleECA2(t);
}
if (chunk.equals(OPRET_MAGIC_ECA3)) {
return handleECA3(t);
}
if (chunk.equals(OPRET_MAGIC_ECA4)) {
return handleECA4(t);
}
if (chunk.equals(OPRET_MAGIC_EC51)) {
return handleEC51(t);
}
if (chunk.equals(OPRET_MAGIC_EC52)) {
return handleEC52(t);
}
return false;
}
@Override
public void pushTransaction(final OPRETTransaction t) {
handleTransaction(t);
}
private void queueOnOPRETId(final MasterVerifyKey k) {
// TODO Auto-generated method stub
}
protected void queueOnOPRETRevoke(final MasterVerifyKey key) {
for (final ListenerRegistration<OPRETECEventListener> registration : opReturnChangeListeners) {
registration.executor.execute(() -> registration.listener.onOPRETRevoke(key));
}
@ -212,7 +488,7 @@ public class OPRETECParser extends OPRETBaseHandler {
return ListenerRegistration.removeFromList(listener, opReturnChangeListeners);
}
public void removeVerifyKey(final VerifyKey key) {
public void removeVerifyKey(final MasterVerifyKey key) {
final List<Byte> hash = Bytes.asList(key.getShortHash());
if (!verifyKeys.containsKey(hash)) {

View file

@ -0,0 +1,131 @@
package org.tcpid.opretj;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.libsodium.jni.NaCl.sodium;
import static org.libsodium.jni.SodiumConstants.NONCE_BYTES;
import static org.libsodium.jni.SodiumConstants.SECRETKEY_BYTES;
import java.math.BigInteger;
import java.util.Arrays;
import org.bitcoinj.core.Utils;
import org.junit.Test;
import org.libsodium.jni.Sodium;
import org.libsodium.jni.crypto.Hash;
import org.libsodium.jni.crypto.Util;
import org.libsodium.jni.encoders.Encoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tcpid.key.HMACSHA512256;
import org.tcpid.key.MasterSigningKey;
import org.tcpid.key.MasterVerifyKey;
import com.google.common.primitives.Bytes;
public class TestCrypto {
private final static Logger logger = LoggerFactory.getLogger(TestCrypto.class);
private final static Hash HASH = new Hash();
@Test
public void testDerive() {
assertTrue("NONCE_BYTES > HMACSHA512256.HMACSHA512256_BYTES", NONCE_BYTES <= HMACSHA512256.HMACSHA512256_BYTES);
assertEquals(SECRETKEY_BYTES, HMACSHA512256.HMACSHA512256_BYTES);
final MasterSigningKey msk = new MasterSigningKey(HASH.sha256("TESTSEED".getBytes()));
assertArrayEquals(Utils.HEX.decode("4071b2b3db7cc7aecd0b23608e96f44f08463ea0ee0a0c12f5fa21ff449deb55"),
msk.toBytes());
final MasterSigningKey subkey = msk.getSubKey(1L).getSubKey(2L).getSubKey(3L).getSubKey(4L);
assertArrayEquals(Utils.HEX.decode("00cb0c8748318d27eab65159a2261c028d764c1154fc302b9b046aa2bbefab27"),
subkey.toBytes());
final BigInteger biMSK = new BigInteger(1, msk.toBytes());
BigInteger biSub = new BigInteger(1, subkey.toBytes());
final BigInteger pow2_256 = new BigInteger("10000000000000000000000000000000000000000000000000000000000000000",
16);
final BigInteger biDiff = biSub.subtract(biMSK).mod(pow2_256);
biSub = biMSK.add(biDiff).mod(pow2_256);
final byte[] bisubb = biSub.toByteArray();
assertArrayEquals(bisubb, subkey.toBytes());
}
@Test
public void testSign() {
final MasterSigningKey msk = new MasterSigningKey(HASH.sha256("TESTSEED".getBytes()));
final MasterVerifyKey vk = msk.getMasterVerifyKey();
final byte[] revokemsg = Bytes.concat("Revoke ".getBytes(), vk.toHash());
final byte[] sig = msk.sign(revokemsg);
assertTrue("Verification of signature failed.", vk.verify(revokemsg, sig));
}
@Test
public void testSignEnc() {
final MasterSigningKey msk = new MasterSigningKey(HASH.sha256("TESTSEED".getBytes()));
final MasterVerifyKey vk = msk.getMasterVerifyKey();
byte[] sig = msk.sign(vk.toBytes());
logger.debug("using key {}", Encoder.HEX.encode(vk.toBytes()));
final byte[] noncebytes = Util.zeros(32);
final byte[] sharedkey = HASH.sha256(HASH.sha256(Bytes.concat(vk.toBytes(), noncebytes)));
final byte[] xornonce = Arrays.copyOfRange(HASH.sha256(Bytes.concat(sharedkey, noncebytes)), 0, 24);
logger.debug("xornonce {}", Encoder.HEX.encode(xornonce));
logger.debug("sharedkey {}", Encoder.HEX.encode(sharedkey));
final byte[] cipher = Util.zeros(96);
byte[] msg = Bytes.concat(vk.toBytes(), sig);
assertEquals(96, msg.length);
sodium();
Sodium.crypto_stream_xsalsa20_xor(cipher, msg, 96, xornonce, sharedkey);
assertEquals(96, cipher.length);
logger.debug("Clear : {}", Encoder.HEX.encode(msg));
logger.debug("Cipher: {}", Encoder.HEX.encode(cipher));
msg = Util.zeros(96);
Sodium.crypto_stream_xsalsa20_xor(msg, cipher, 96, xornonce, sharedkey);
final byte[] vkb = Arrays.copyOfRange(msg, 0, 32);
sig = Arrays.copyOfRange(msg, 32, 96);
logger.debug("vkb : {}", Encoder.HEX.encode(vkb));
assertTrue("Verification of signature failed.", vk.verify(vkb, sig));
assertArrayEquals(vk.toBytes(), vkb);
}
@Test
public void testSignEncNoncebytes() {
final MasterSigningKey msk = new MasterSigningKey(HASH.sha256("TESTSEED".getBytes()));
final MasterVerifyKey vk = msk.getMasterVerifyKey();
byte[] sig = msk.sign(vk.toBytes());
logger.debug("nonce: using key {}", Encoder.HEX.encode(vk.toBytes()));
final byte[] noncebytes = Encoder.HEX
.decode("0000000000000000000000000000001100000000000000000000000000000000");
final byte[] sharedkey = HASH.sha256(HASH.sha256(Bytes.concat(vk.toBytes(), noncebytes)));
final byte[] xornonce = Arrays.copyOfRange(HASH.sha256(Bytes.concat(sharedkey, noncebytes)), 0, 24);
logger.debug("nonce: nonce {}", Encoder.HEX.encode(xornonce));
logger.debug("nonce: sharedkey {}", Encoder.HEX.encode(sharedkey));
final byte[] cipher = Util.zeros(96);
byte[] msg = Bytes.concat(vk.toBytes(), sig);
assertEquals(96, msg.length);
sodium();
Sodium.crypto_stream_xsalsa20_xor(cipher, msg, 96, xornonce, sharedkey);
assertEquals(96, cipher.length);
logger.debug("nonce: Clear : {}", Encoder.HEX.encode(msg));
logger.debug("nonce: Cipher: {}", Encoder.HEX.encode(cipher));
msg = Util.zeros(96);
Sodium.crypto_stream_xsalsa20_xor(msg, cipher, 96, xornonce, sharedkey);
final byte[] vkb = Arrays.copyOfRange(msg, 0, 32);
sig = Arrays.copyOfRange(msg, 32, 96);
logger.debug("nonce: vkb : {}", Encoder.HEX.encode(vkb));
assertTrue("nonce: Verification of signature failed.", vk.verify(vkb, sig));
assertArrayEquals(vk.toBytes(), vkb);
}
}

View file

@ -0,0 +1,146 @@
/**
*
*/
package org.tcpid.opretj;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Sha256Hash;
import org.junit.Test;
import org.libsodium.jni.encoders.Encoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tcpid.key.MasterVerifyKey;
import com.google.common.primitives.Bytes;
public class TestECA1 {
private final static Logger logger = LoggerFactory.getLogger(TestECA1.class);
/**
* Test method for
* {@link org.tcpid.opretj.OPRETECParser#pushTransaction(org.tcpid.opretj.OPRETTransaction)}.
*/
@Test
public void testPushTransaction() {
logger.debug("testPushTransaction");
final byte[] cipher = Encoder.HEX.decode(
"bed9e277c3fde807eecb2100e2a4c9ec1067891b9f021e3bfbc599a3676048598e7c9801d94d9765cb965e64cfb9f493d7ae332bc85affb8bb0337b6835c51d156005db43ab8ea9b988632bfadcaee7dabf08709be248f5354d59a98e53f0cda");
final byte[] vkb = Encoder.HEX.decode("fb2e360caf811b3aaf534d0458c2a2ca3e1f213b244a6f83af1ab50eddacdd8c");
final MasterVerifyKey mvk = new MasterVerifyKey(vkb);
final byte[] vkbsha96 = Arrays.copyOfRange(Sha256Hash.of(vkb).getBytes(), 0, 12);
final byte[] nullbyte = {};
List<List<Byte>> opret_data = new ArrayList<>();
opret_data.add(Bytes.asList(Encoder.HEX.decode("eca1")));
opret_data.add(Bytes.asList(Arrays.copyOfRange(cipher, 0, 48)));
opret_data.add(Bytes.asList(vkbsha96));
final OPRETTransaction t1 = new OPRETTransaction(Sha256Hash.of(nullbyte), Sha256Hash.of(nullbyte), opret_data);
opret_data = new ArrayList<>();
opret_data.add(Bytes.asList(Encoder.HEX.decode("eca2")));
opret_data.add(Bytes.asList(Arrays.copyOfRange(cipher, 48, 96)));
opret_data.add(Bytes.asList(vkbsha96));
final OPRETTransaction t2 = new OPRETTransaction(Sha256Hash.of(nullbyte), Sha256Hash.of(nullbyte), opret_data);
opret_data = new ArrayList<>();
opret_data.add(Bytes.asList(Encoder.HEX.decode("eca2")));
opret_data.add(Bytes.asList(Arrays.copyOfRange(cipher, 0, 48)));
opret_data.add(Bytes.asList(vkbsha96));
final OPRETTransaction t3 = new OPRETTransaction(Sha256Hash.of(nullbyte), Sha256Hash.of(nullbyte), opret_data);
opret_data = new ArrayList<>();
opret_data.add(Bytes.asList(Encoder.HEX.decode("eca1")));
opret_data.add(Bytes.asList(Arrays.copyOfRange(cipher, 48, 96)));
opret_data.add(Bytes.asList(vkbsha96));
final OPRETTransaction t4 = new OPRETTransaction(Sha256Hash.of(nullbyte), Sha256Hash.of(nullbyte), opret_data);
final OPRETECParser parser = new OPRETECParser();
parser.addVerifyKey(mvk, 0);
assertFalse(parser.handleTransaction(t2));
assertFalse(parser.handleTransaction(t3));
assertFalse(parser.handleTransaction(t4));
assertTrue(parser.handleTransaction(t1));
mvk.clearSubKeys();
assertFalse(parser.handleTransaction(t1));
assertFalse(parser.handleTransaction(t3));
assertFalse(parser.handleTransaction(t4));
assertTrue(parser.handleTransaction(t2));
mvk.clearSubKeys();
assertFalse(parser.handleTransaction(t1));
assertFalse(parser.handleTransaction(t4));
assertFalse(parser.handleTransaction(t3));
assertTrue(parser.handleTransaction(t2));
mvk.clearSubKeys();
assertFalse(parser.handleTransaction(t2));
assertFalse(parser.handleTransaction(t4));
assertFalse(parser.handleTransaction(t3));
assertTrue(parser.handleTransaction(t1));
}
/**
* Test method for
* {@link org.tcpid.opretj.OPRETECParser#pushTransaction(org.tcpid.opretj.OPRETTransaction)}.
*/
@Test
public void testPushTransactionWithNonce() {
logger.debug("testPushTransactionWithNonce");
final byte[] cipher = Encoder.HEX.decode(
"24f99184d03a6ffa5826bd9300a7fb1cff264600f335b3c6042f15cb4d3d9019fa2a9c905cdf6f6c80178def845f0340e6d2e55a7dee433a5af984760adc23e187734e5e4e76aa22f3acab172262633139b6dcd11229fe2385661a70d6c206c0");
final byte[] vkb = Encoder.HEX.decode("fb2e360caf811b3aaf534d0458c2a2ca3e1f213b244a6f83af1ab50eddacdd8c");
final MasterVerifyKey mvk = new MasterVerifyKey(vkb);
final byte[] vkbsha96 = Arrays.copyOfRange(Sha256Hash.of(vkb).getBytes(), 0, 12);
final byte[] nullbyte = {};
List<List<Byte>> opret_data = new ArrayList<>();
opret_data.add(Bytes.asList(Encoder.HEX.decode("eca1")));
final byte[] byte1f = { (byte) 0x11 };
opret_data.add(Bytes.asList(Bytes.concat(Arrays.copyOfRange(cipher, 0, 48), byte1f)));
opret_data.add(Bytes.asList(vkbsha96));
final OPRETTransaction t1 = new OPRETTransaction(Sha256Hash.of(nullbyte), Sha256Hash.of(nullbyte), opret_data);
opret_data = new ArrayList<>();
opret_data.add(Bytes.asList(Encoder.HEX.decode("eca2")));
opret_data.add(Bytes.asList(Arrays.copyOfRange(cipher, 48, 96)));
opret_data.add(Bytes.asList(vkbsha96));
final OPRETTransaction t2 = new OPRETTransaction(Sha256Hash.of(nullbyte), Sha256Hash.of(nullbyte), opret_data);
final OPRETECParser parser = new OPRETECParser();
parser.addVerifyKey(mvk, 0);
assertFalse(parser.handleTransaction(t2));
assertTrue(parser.handleTransaction(t1));
mvk.clearSubKeys();
assertFalse(parser.handleTransaction(t1));
assertTrue(parser.handleTransaction(t2));
mvk.clearSubKeys();
assertFalse(parser.handleTransaction(t1));
assertTrue(parser.handleTransaction(t2));
mvk.clearSubKeys();
assertFalse(parser.handleTransaction(t2));
assertTrue(parser.handleTransaction(t1));
}
}