diff --git a/README.md b/README.md index 9bc307d..6452b4d 100644 --- a/README.md +++ b/README.md @@ -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 for a Key encrypted with the encryption key EK +OP_RETURN 0xEC10 || Box_ENC() 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. diff --git a/opret-testapp/src/main/java/org/tcpid/opretj/testapp/App.java b/opret-testapp/src/main/java/org/tcpid/opretj/testapp/App.java index d122b5d..b874d7c 100644 --- a/opret-testapp/src/main/java/org/tcpid/opretj/testapp/App.java +++ b/opret-testapp/src/main/java/org/tcpid/opretj/testapp/App.java @@ -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); diff --git a/opretj/.settings/org.eclipse.core.resources.prefs b/opretj/.settings/org.eclipse.core.resources.prefs index e9441bb..f9fe345 100644 --- a/opretj/.settings/org.eclipse.core.resources.prefs +++ b/opretj/.settings/org.eclipse.core.resources.prefs @@ -1,3 +1,4 @@ eclipse.preferences.version=1 encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 encoding/=UTF-8 diff --git a/opretj/pom.xml b/opretj/pom.xml index 8edcaac..eac0dee 100644 --- a/opretj/pom.xml +++ b/opretj/pom.xml @@ -7,16 +7,16 @@ 0.0.2-SNAPSHOT opretj - - - oss-sonatype - oss-sonatype - https://oss.sonatype.org/content/repositories/snapshots/ - - true - - - + + + oss-sonatype + oss-sonatype + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + @@ -30,7 +30,7 @@ junit junit - 3.8.1 + 4.11 test @@ -64,6 +64,15 @@ 1.8 + + + org.apache.maven.plugins + maven-surefire-plugin + + alphabetical + + + @@ -75,5 +84,4 @@ https://github.com/haraldh/opretj/issues - \ No newline at end of file diff --git a/opretj/src/main/java/org/tcpid/key/MasterSigningKey.java b/opretj/src/main/java/org/tcpid/key/MasterSigningKey.java index 023ba5b..e7a1b59 100644 --- a/opretj/src/main/java/org/tcpid/key/MasterSigningKey.java +++ b/opretj/src/main/java/org/tcpid/key/MasterSigningKey.java @@ -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); } diff --git a/opretj/src/main/java/org/tcpid/key/MasterVerifyKey.java b/opretj/src/main/java/org/tcpid/key/MasterVerifyKey.java index c96d43d..47b11a8 100644 --- a/opretj/src/main/java/org/tcpid/key/MasterVerifyKey.java +++ b/opretj/src/main/java/org/tcpid/key/MasterVerifyKey.java @@ -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 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"); } diff --git a/opretj/src/main/java/org/tcpid/opretj/OPRETECEventListener.java b/opretj/src/main/java/org/tcpid/opretj/OPRETECEventListener.java index 07d0e33..f46db24 100644 --- a/opretj/src/main/java/org/tcpid/opretj/OPRETECEventListener.java +++ b/opretj/src/main/java/org/tcpid/opretj/OPRETECEventListener.java @@ -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); } diff --git a/opretj/src/main/java/org/tcpid/opretj/OPRETECParser.java b/opretj/src/main/java/org/tcpid/opretj/OPRETECParser.java index dbf352c..747e336 100644 --- a/opretj/src/main/java/org/tcpid/opretj/OPRETECParser.java +++ b/opretj/src/main/java/org/tcpid/opretj/OPRETECParser.java @@ -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 OPRET_MAGIC = Bytes.asList(Utils.HEX.decode("ec0f")); + private static final List OPRET_MAGIC_EC1C = Bytes.asList(Utils.HEX.decode("ec1c")); + private static final List OPRET_MAGIC_EC1D = Bytes.asList(Utils.HEX.decode("ec1d")); + private static final List OPRET_MAGIC_ECA1 = Bytes.asList(Utils.HEX.decode("eca1")); + private static final List OPRET_MAGIC_ECA2 = Bytes.asList(Utils.HEX.decode("eca2")); + private static final List OPRET_MAGIC_ECA3 = Bytes.asList(Utils.HEX.decode("eca3")); + private static final List OPRET_MAGIC_ECA4 = Bytes.asList(Utils.HEX.decode("eca4")); + private static final List OPRET_MAGIC_EC51 = Bytes.asList(Utils.HEX.decode("ec51")); + private static final List OPRET_MAGIC_EC52 = Bytes.asList(Utils.HEX.decode("ec52")); + private static final List 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 merkleHashMap = Collections.synchronizedMap(new HashMap<>()); protected final Map transHashMap = Collections.synchronizedMap(new HashMap<>()); + protected final Map, List> transA1HashMap = Collections + .synchronizedMap(new HashMap<>()); + protected final Map, List> transA2HashMap = Collections + .synchronizedMap(new HashMap<>()); + protected final Map, List> transA3HashMap = Collections + .synchronizedMap(new HashMap<>()); + protected final Map, List> transA4HashMap = Collections + .synchronizedMap(new HashMap<>()); + protected final Map, List> trans51HashMap = Collections + .synchronizedMap(new HashMap<>()); + protected final Map, List> trans52HashMap = Collections + .synchronizedMap(new HashMap<>()); - protected final Map, List> verifyKeys = Collections.synchronizedMap(new HashMap<>()); + protected final Map, List> verifyKeys = Collections.synchronizedMap(new HashMap<>()); private final CopyOnWriteArrayList> opReturnChangeListeners = new CopyOnWriteArrayList<>(); @@ -80,10 +104,10 @@ public class OPRETECParser extends OPRETBaseHandler { opReturnChangeListeners.add(new ListenerRegistration(listener, Threading.SAME_THREAD)); } - public void addVerifyKey(final VerifyKey key, final long earliestTime) { + public void addVerifyKey(final MasterVerifyKey key, final long earliestTime) { final List hash = Bytes.asList(key.getShortHash()); if (!verifyKeys.containsKey(hash)) { - verifyKeys.put(hash, new ArrayList()); + verifyKeys.put(hash, new ArrayList()); } 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> opret_data = new ArrayList<>(t.opretData); - logger.debug("checking {}", opret_data); - - if (opret_data.size() != 3) { - return false; - } - - List 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 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 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 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 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()); + } + 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 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()); + } + 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 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 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 hash = Bytes.asList(key.getShortHash()); if (!verifyKeys.containsKey(hash)) { diff --git a/opretj/src/test/java/org/tcpid/opretj/TestCrypto.java b/opretj/src/test/java/org/tcpid/opretj/TestCrypto.java new file mode 100644 index 0000000..0cf2f37 --- /dev/null +++ b/opretj/src/test/java/org/tcpid/opretj/TestCrypto.java @@ -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); + } +} diff --git a/opretj/src/test/java/org/tcpid/opretj/TestECA1.java b/opretj/src/test/java/org/tcpid/opretj/TestECA1.java new file mode 100644 index 0000000..dca7e5c --- /dev/null +++ b/opretj/src/test/java/org/tcpid/opretj/TestECA1.java @@ -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> 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> 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)); + } +}