Refactor to use BouncyCastle Ed25519/X25519, and more...

Remove old whispersystems, etc. *25519 and use new v1.61 BouncyCastle.

Fix proxy forging private key derivation from X25519 shared secret.
Also include Javascript test version for comparison.

Fix block rewards for proxy forging.

Add extra useful info to API call GET /admin/forgingaccounts.
Fix API response to POST/DELETE /admin/forgingaccounts when
passed invalid private keys.

Added block rewards and account flags to testchain config.

Tests to cover changes above.
This commit is contained in:
catbref
2019-04-17 12:32:03 +01:00
parent 8e74884536
commit d1c547f24a
68 changed files with 1075 additions and 5564 deletions

View File

@@ -1,12 +1,23 @@
package org.qora.test;
import org.junit.Test;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.BlockChain;
import org.qora.crypto.Crypto;
import org.qora.test.common.Common;
import static org.junit.Assert.*;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import org.bitcoinj.core.Base58;
import org.bouncycastle.crypto.agreement.X25519Agreement;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
import com.google.common.hash.HashCode;
public class CryptoTests extends Common {
@@ -37,4 +48,74 @@ public class CryptoTests extends Common {
assertEquals(expected, Crypto.toAddress(publicKey));
}
@Test
public void verifySignature() {
final String privateKey58 = "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6";
final String message58 = "111FDmMy7u7ChH3SNLNYoUqE9eQRDVKGzhYTAU7XJRVZ7L966aKdDFBeD5WBQP372Lgpdbt4L8HuPobB1CWbJzdUqa72MYVA8A8pmocQQpzRsC5Kreif94yiScTDnnvCWcNERj9J2sqTH12gVdeeLt9Ery7HZFi6tDyysTLBkWfmDjuLnSfDKc7xeqZFkMSG1oatPedzrsDtrBZ";
final String expectedSignature58 = "41g1hidZGbNn8xCCH41j1V1tD9iUwz7LCF4UcH19eindYyBnjKxfHdPm9qyRvLYFmXp8PV8YXzMXWUUngmqHo5Ho";
final byte[] privateKey = Base58.decode(privateKey58);
PrivateKeyAccount account = new PrivateKeyAccount(null, privateKey);
byte[] message = Base58.decode(message58);
byte[] signature = account.sign(message);
assertEquals(expectedSignature58, Base58.encode(signature));
assertTrue(account.verify(signature, message));
}
@Test
public void testBCseed() throws NoSuchAlgorithmException, NoSuchProviderException {
final String privateKey58 = "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6";
final String publicKey58 = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP";
final byte[] privateKey = Base58.decode(privateKey58);
PrivateKeyAccount account = new PrivateKeyAccount(null, privateKey);
String expected58 = publicKey58;
String actual58 = Base58.encode(account.getPublicKey());
assertEquals("qora-core generated public key incorrect", expected58, actual58);
Ed25519PrivateKeyParameters privateKeyParams = new Ed25519PrivateKeyParameters(privateKey, 0);
Ed25519PublicKeyParameters publicKeyParams = privateKeyParams.generatePublicKey();
actual58 = Base58.encode(publicKeyParams.getEncoded());
assertEquals("BouncyCastle generated public key incorrect", expected58, actual58);
}
@Test
public void testBCSharedSecret() throws NoSuchAlgorithmException, NoSuchProviderException {
final byte[] ourPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
final byte[] theirPublicKey = Base58.decode("2sbcMmVKke5inS4yrbeoG6Cyw2mZCptQNjyWgnY4YHaF");
final String expectedProxyPrivateKey = "EZhKy6wEh1ncQsvx6x3yV2sqjjsoU1bTTqrMcFLjLmp4";
X25519PrivateKeyParameters ourPrivateKeyParams = new X25519PrivateKeyParameters(ourPrivateKey, 0);
X25519PublicKeyParameters theirPublicKeyParams = new X25519PublicKeyParameters(theirPublicKey, 0);
byte[] sharedSecret = new byte[32];
X25519Agreement keyAgree = new X25519Agreement();
keyAgree.init(ourPrivateKeyParams);
keyAgree.calculateAgreement(theirPublicKeyParams, sharedSecret, 0);
String proxyPrivateKey = Base58.encode(Crypto.digest(sharedSecret));
assertEquals("proxy private key incorrect", expectedProxyPrivateKey, proxyPrivateKey);
}
@Test
public void testSharedSecret() throws NoSuchAlgorithmException, NoSuchProviderException {
final byte[] ourPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
final byte[] theirPublicKey = Base58.decode("2sbcMmVKke5inS4yrbeoG6Cyw2mZCptQNjyWgnY4YHaF");
final String expectedProxyPrivateKey = "EZhKy6wEh1ncQsvx6x3yV2sqjjsoU1bTTqrMcFLjLmp4";
PrivateKeyAccount generator = new PrivateKeyAccount(null, ourPrivateKey);
byte[] sharedSecret = generator.getSharedSecret(theirPublicKey);
String proxyPrivateKey = Base58.encode(Crypto.digest(sharedSecret));
assertEquals("proxy private key incorrect", expectedProxyPrivateKey, proxyPrivateKey);
}
}

View File

@@ -0,0 +1,77 @@
package org.qora.test.forging;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qora.account.PrivateKeyAccount;
import org.qora.asset.Asset;
import org.qora.block.BlockChain;
import org.qora.block.BlockChain.RewardsByHeight;
import org.qora.block.BlockGenerator;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.AccountUtils;
import org.qora.test.common.Common;
public class RewardTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void testSimpleReward() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA);
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
BigDecimal firstReward = BlockChain.getInstance().getBlockRewardsByHeight().get(0).reward;
BlockGenerator.generateTestingBlock(repository, forgingAccount);
BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORA).add(firstReward);
AccountUtils.assertBalance(repository, "alice", Asset.QORA, expectedBalance);
}
}
@Test
public void testRewards() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA);
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
List<RewardsByHeight> rewards = BlockChain.getInstance().getBlockRewardsByHeight();
int rewardIndex = rewards.size() - 1;
RewardsByHeight rewardInfo = rewards.get(rewardIndex);
BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORA);
for (int height = rewardInfo.height; height > 1; --height) {
if (height < rewardInfo.height) {
--rewardIndex;
rewardInfo = rewards.get(rewardIndex);
}
BlockGenerator.generateTestingBlock(repository, forgingAccount);
expectedBalance = expectedBalance.add(rewardInfo.reward);
}
AccountUtils.assertBalance(repository, "alice", Asset.QORA, expectedBalance);
}
}
}

View File

@@ -0,0 +1,100 @@
// Generated by CoffeeScript 1.8.0
(function() {
var ALPHABET, ALPHABET_MAP, Base58, i;
Base58 = (typeof module !== "undefined" && module !== null ? module.exports : void 0) || (window.Base58 = {});
ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
ALPHABET_MAP = {};
i = 0;
while (i < ALPHABET.length) {
ALPHABET_MAP[ALPHABET.charAt(i)] = i;
i++;
}
Base58.encode = function(buffer) {
var carry, digits, j;
if (buffer.length === 0) {
return "";
}
i = void 0;
j = void 0;
digits = [0];
i = 0;
while (i < buffer.length) {
j = 0;
while (j < digits.length) {
digits[j] <<= 8;
j++;
}
digits[0] += buffer[i];
carry = 0;
j = 0;
while (j < digits.length) {
digits[j] += carry;
carry = (digits[j] / 58) | 0;
digits[j] %= 58;
++j;
}
while (carry) {
digits.push(carry % 58);
carry = (carry / 58) | 0;
}
i++;
}
i = 0;
while (buffer[i] === 0 && i < buffer.length - 1) {
digits.push(0);
i++;
}
return digits.reverse().map(function(digit) {
return ALPHABET[digit];
}).join("");
};
Base58.decode = function(string) {
var bytes, c, carry, j;
if (string.length === 0) {
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(0);
}
i = void 0;
j = void 0;
bytes = [0];
i = 0;
while (i < string.length) {
c = string[i];
if (!(c in ALPHABET_MAP)) {
throw "Base58.decode received unacceptable input. Character '" + c + "' is not in the Base58 alphabet.";
}
j = 0;
while (j < bytes.length) {
bytes[j] *= 58;
j++;
}
bytes[0] += ALPHABET_MAP[c];
carry = 0;
j = 0;
while (j < bytes.length) {
bytes[j] += carry;
carry = bytes[j] >> 8;
bytes[j] &= 0xff;
++j;
}
while (carry) {
bytes.push(carry & 0xff);
carry >>= 8;
}
i++;
}
i = 0;
while (string[i] === "1" && i < string.length - 1) {
bytes.push(0);
i++;
}
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(bytes.reverse());
};
}).call(this);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<script src="Base58.js"></script>
<script src="nacl_factory.js"></script>
<script>
nacl_factory.instantiate(function (nacl) {
var mintingAccountPrk = 'A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6';
// var recipientAccountPuk = 'C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry';
var recipientAccountPuk = '2sbcMmVKke5inS4yrbeoG6Cyw2mZCptQNjyWgnY4YHaF';
mintingAccountPrk = Base58.decode(mintingAccountPrk);
recipientAccountPuk = Base58.decode(recipientAccountPuk);
var mintingKeyPair = nacl.crypto_box_keypair_from_raw_sk(mintingAccountPrk);
// Expecting: A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6
console.log("minting private key (for confirmation): " + Base58.encode(mintingKeyPair.boxSk));
// This WILL NOT be: 2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP because this is an X25519 keypair, not Ed25519 keypair?
// console.log("minting PUBLIC key: " + Base58.encode(mintingKeyPair.boxPk));
var sharedSecret = nacl.crypto_scalarmult(mintingKeyPair.boxSk, recipientAccountPuk);
console.log("shared secret (for debugging): " + Base58.encode(sharedSecret));
console.log(sharedSecret); // log as Uint8Array
var proxyPrivateKey = nacl.crypto_hash_sha256(sharedSecret)
console.log("proxy private key: " + Base58.encode(proxyPrivateKey));
var proxyKeyPair = nacl.crypto_sign_seed_keypair(proxyPrivateKey);
console.log("proxy public key: " + Base58.encode(proxyKeyPair.signPk));
});
</script>
</head>
<body>
</body>
</html>

View File

@@ -21,9 +21,15 @@
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "owner": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }
]
},
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },
{ "height": 21, "reward": 1 }
],
"featureTriggers": {
"messageHeight": 0,
"atHeight": 0,