Add AgeWittness domain (WIP)

This commit is contained in:
Manfred Karrer 2017-09-23 10:08:32 -05:00
parent fcd3106eb4
commit 007142ed9b
No known key found for this signature in database
GPG Key ID: 401250966A6B2C46
7 changed files with 438 additions and 0 deletions

View File

@ -20,6 +20,7 @@ package io.bisq.common.crypto;
import lombok.extern.slf4j.Slf4j;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@ -29,5 +30,11 @@ public class CryptoUtils {
final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded());
return Base64.getEncoder().encodeToString(x509EncodedKeySpec.getEncoded());
}
public static byte[] getSalt(int size) {
byte[] salt = new byte[size];
new SecureRandom().nextBytes(salt);
return salt;
}
}

View File

@ -17,9 +17,11 @@
package io.bisq.core.payment;
import io.bisq.common.crypto.CryptoUtils;
import io.bisq.common.locale.TradeCurrency;
import io.bisq.common.proto.ProtoUtil;
import io.bisq.common.proto.persistable.PersistablePayload;
import io.bisq.common.util.Utilities;
import io.bisq.core.payment.payload.PaymentAccountPayload;
import io.bisq.core.payment.payload.PaymentMethod;
import io.bisq.core.proto.CoreProtoResolver;
@ -57,6 +59,15 @@ public abstract class PaymentAccount implements PersistablePayload {
@Nullable
protected TradeCurrency selectedTradeCurrency;
@Setter
@Nullable
protected PaymentAccountAgeWitness paymentAccountAgeWitness;
// TODO add to PB!
@Setter
@Nullable
protected byte[] salt;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -70,6 +81,9 @@ public abstract class PaymentAccount implements PersistablePayload {
id = UUID.randomUUID().toString();
creationDate = new Date().getTime();
paymentAccountPayload = getPayload();
// We set by default a random salt. User can set salt as well by hex string
salt = CryptoUtils.getSalt(32); // 256 bit
}
@ -142,4 +156,20 @@ public abstract class PaymentAccount implements PersistablePayload {
}
protected abstract PaymentAccountPayload getPayload();
// TODO make abstract
// Identifying data of payment account (e.g. IBAN).
// This is critical code for verifying age of payment account.
// Any change would break validation of historical data!
public byte[] getAgeWitnessInputData() {
return new byte[0];
}
public void setSaltAsHex(String saltAsHex) {
this.salt = Utilities.decodeFromHex(saltAsHex);
}
public String getSaltAsHex() {
return Utilities.bytesAsHexString(salt);
}
}

View File

@ -0,0 +1,135 @@
/*
* This file is part of bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.payment;
import com.google.protobuf.Message;
import io.bisq.common.crypto.Sig;
import io.bisq.network.p2p.storage.payload.LazyProcessedStoragePayload;
import io.bisq.network.p2p.storage.payload.PersistedStoragePayload;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.security.PublicKey;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;
// Object has 118 raw bytes (not PB size)
@Slf4j
@EqualsAndHashCode(exclude = {"signaturePubKey"})
@Value
public class PaymentAccountAgeWitness implements LazyProcessedStoragePayload, PersistedStoragePayload {
private final byte[] hash; // 32 bytes
private final byte[] hashOfPubKey; // 32 bytes
private final byte[] signature; // 46 bytes
private final long tradeDate; // 8 byte
// Only used as cache for getOwnerPubKey
transient private final PublicKey signaturePubKey;
// Should be only used in emergency case if we need to add data but do not want to break backward compatibility
// at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new
// field in a class would break that hash and therefore break the storage mechanism.
@Nullable
private Map<String, String> extraDataMap;
public PaymentAccountAgeWitness(byte[] hash,
byte[] hashOfPubKey,
byte[] signature,
long tradeDate) {
this(hash,
hashOfPubKey,
signature,
tradeDate,
null);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
PaymentAccountAgeWitness(byte[] hash,
byte[] hashOfPubKey,
byte[] signature,
long tradeDate,
@Nullable Map<String, String> extraDataMap) {
this.hash = hash;
this.hashOfPubKey = hashOfPubKey;
this.signature = signature;
this.tradeDate = tradeDate;
this.extraDataMap = extraDataMap;
signaturePubKey = Sig.getPublicKeyFromBytes(signature);
}
// TODO
@Override
public Message toProtoMessage() {
return null;
}
/* @Override
public PB.StoragePayload toProtoMessage() {
final PB.PaymentAccountAgeWitness.Builder builder = PB.PaymentAccountAgeWitness.newBuilder()
.setSignaturePubKeyBytes(ByteString.copyFrom(hash))
.setSignaturePubKeyBytes(ByteString.copyFrom(pubKeyBytes))
.setSignaturePubKeyBytes(ByteString.copyFrom(signaturePubKeyBytes));
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
return PB.StoragePayload.newBuilder().setPaymentAccountAgeWitness(builder).build();
}
public PB.PaymentAccountAgeWitness toProtoPaymentAccountAgeWitness() {
return toProtoMessage().getPaymentAccountAgeWitness();
}
public static PaymentAccountAgeWitness fromProto(PB.PaymentAccountAgeWitness proto) {
return new PaymentAccountAgeWitness(
OfferPayload.Direction.fromProto(proto.getDirection()),
proto.getHash().toByteArray(),
proto.getPubKeyBytes().toByteArray(),
proto.getSignaturePubKeyBytes().toByteArray(),
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap());
}*/
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public long getTTL() {
return TimeUnit.DAYS.toMillis(30);
}
@Override
public PublicKey getOwnerPubKey() {
return signaturePubKey;
}
//TODO impl. here or in caller?
// We allow max 1 day time difference
public boolean isTradeDateValid() {
return new Date().getTime() - tradeDate < TimeUnit.DAYS.toMillis(1);
}
}

View File

@ -0,0 +1,144 @@
/*
* This file is part of bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.payment;
import io.bisq.common.crypto.CryptoException;
import io.bisq.common.crypto.Hash;
import io.bisq.common.crypto.KeyRing;
import io.bisq.common.crypto.Sig;
import io.bisq.common.util.Utilities;
import io.bisq.core.trade.Trade;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.bitcoinj.core.Sha256Hash;
import javax.inject.Inject;
import java.math.BigInteger;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.concurrent.TimeUnit;
@Slf4j
public class PaymentAccountAgeWitnessService {
private KeyRing keyRing;
@Inject
public PaymentAccountAgeWitnessService(KeyRing keyRing) {
this.keyRing = keyRing;
}
public PaymentAccountAgeWitness getPaymentAccountWitness(PaymentAccount paymentAccount, Trade trade) throws CryptoException {
byte[] ageWitnessInputData = paymentAccount.getAgeWitnessInputData();
byte[] salt = paymentAccount.getSalt();
final byte[] combined = ArrayUtils.addAll(ageWitnessInputData, salt);
byte[] hash = Sha256Hash.hash(combined);
byte[] signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), hash);
long tradeDate = trade.getTakeOfferDate().getTime();
byte[] hashOfPubKey = Sha256Hash.hash(keyRing.getPubKeyRing().getSignaturePubKeyBytes());
return new PaymentAccountAgeWitness(hash,
hashOfPubKey,
signature,
tradeDate);
}
boolean verifyAgeWitness(byte[] peersAgeWitnessInputData,
PaymentAccountAgeWitness witness,
byte[] peersSalt,
PublicKey peersPublicKey,
int nonce,
byte[] signatureOfNonce) throws CryptoException {
// Check if trade date in witness is not older than the release date of that feature (was added in v0.6)
Date ageWitnessReleaseDate = new GregorianCalendar(2017, 9, 23).getTime();
if (!isTradeDateAfterReleaseDate(witness.getTradeDate(), ageWitnessReleaseDate))
return false;
// Check if peer's pubkey is matching the hash in the witness data
if (!verifyPubKeyHash(witness.getHashOfPubKey(), peersPublicKey))
return false;
final byte[] combined = ArrayUtils.addAll(peersAgeWitnessInputData, peersSalt);
byte[] hash = Sha256Hash.hash(combined);
// Check if the hash in the witness data matches the peer's payment account input data + salt
if (!verifyWitnessHash(witness.getHash(), hash))
return false;
// Check if the witness signature is correct
if (!verifySignature(peersPublicKey, hash, witness.getSignature()))
return false;
// Check if the signature of the nonce is correct
return !verifySignatureOfNonce(peersPublicKey, nonce, signatureOfNonce);
}
boolean isTradeDateAfterReleaseDate(long tradeDateAsLong, Date ageWitnessReleaseDate) {
// Release date minus 1 day as tolerance for not synced clocks
Date releaseDateWithTolerance = new Date(ageWitnessReleaseDate.getTime() - TimeUnit.DAYS.toMillis(1));
final Date tradeDate = new Date(tradeDateAsLong);
final boolean result = tradeDate.after(releaseDateWithTolerance);
if (!result)
log.warn("Trade date is earlier than release date of ageWitness minus 1 day. " +
"ageWitnessReleaseDate={}, tradeDate={}", ageWitnessReleaseDate, tradeDate);
return result;
}
boolean verifyPubKeyHash(byte[] hashOfPubKey,
PublicKey peersPublicKey) {
final boolean result = Arrays.equals(Hash.getHash(Sig.getPublicKeyBytes(peersPublicKey)), hashOfPubKey);
if (!result)
log.warn("hashOfPubKey is not matching peers peersPublicKey. " +
"hashOfPubKey={}, peersPublicKey={}", Utilities.bytesAsHexString(hashOfPubKey), peersPublicKey);
return result;
}
boolean verifyWitnessHash(byte[] witnessHash,
byte[] hash) {
final boolean result = Arrays.equals(witnessHash, hash);
if (!result)
log.warn("witnessHash is not matching peers hash. " +
"witnessHash={}, hash={}", Utilities.bytesAsHexString(witnessHash), Utilities.bytesAsHexString(hash));
return result;
}
boolean verifySignature(PublicKey peersPublicKey, byte[] data, byte[] signature) {
try {
return Sig.verify(peersPublicKey, data, signature);
} catch (CryptoException e) {
log.warn("Signature of PaymentAccountAgeWitness is not correct. " +
"peersPublicKey={}, data={}, signature={}",
peersPublicKey, Utilities.bytesAsHexString(data), Utilities.bytesAsHexString(signature));
return false;
}
}
boolean verifySignatureOfNonce(PublicKey peersPublicKey, int nonce, byte[] signature) {
try {
return Sig.verify(peersPublicKey, BigInteger.valueOf(nonce).toByteArray(), signature);
} catch (CryptoException e) {
log.warn("Signature of nonce is not correct. " +
"peersPublicKey={}, nonce={}, signature={}",
peersPublicKey, nonce, Utilities.bytesAsHexString(signature));
return false;
}
}
}

View File

@ -22,7 +22,9 @@ import io.bisq.core.payment.payload.PaymentAccountPayload;
import io.bisq.core.payment.payload.PaymentMethod;
import io.bisq.core.payment.payload.SepaAccountPayload;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.ArrayUtils;
import java.nio.charset.Charset;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@ -77,4 +79,11 @@ public final class SepaAccount extends CountryBasedPaymentAccount implements Ban
public void removeAcceptedCountry(String countryCode) {
((SepaAccountPayload) paymentAccountPayload).removeAcceptedCountry(countryCode);
}
@Override
public byte[] getAgeWitnessInputData() {
// We don't add holderName because we don't want to break age validation if the user recreates an account with
// slight changes in holder name (e.g. add or remove middle name)
return ArrayUtils.addAll(getIban().getBytes(Charset.forName("UTF-8")), getBic().getBytes(Charset.forName("UTF-8")));
}
}

View File

@ -31,6 +31,10 @@ import lombok.extern.slf4j.Slf4j;
public abstract class PaymentAccountPayload implements NetworkPayload {
protected final String paymentMethodId;
protected final String id;
// That is problematic and should be removed in next hard fork.
// Any change in maxTradePeriod would make existing payment accounts incompatible.
// TODO prepare backward compatible change
protected final long maxTradePeriod;

View File

@ -0,0 +1,109 @@
package io.bisq.core.payment;
import io.bisq.common.crypto.CryptoException;
import io.bisq.common.crypto.Hash;
import io.bisq.common.crypto.Sig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.bitcoinj.core.Sha256Hash;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.util.Date;
import java.util.GregorianCalendar;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
@Slf4j
public class PaymentAccountAgeWitnessServiceTest {
private PublicKey publicKey;
private KeyPair keypair;
private PaymentAccountAgeWitnessService service;
@Before
public void setup() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException {
service = new PaymentAccountAgeWitnessService(null);
keypair = Sig.generateKeyPair();
publicKey = keypair.getPublic();
}
@After
public void tearDown() throws IOException {
}
@Test
public void testIsTradeDateAfterReleaseDate() throws CryptoException {
Date ageWitnessReleaseDate = new GregorianCalendar(2017, 9, 23).getTime();
Date tradeDate = new GregorianCalendar(2017, 10, 1).getTime();
assertTrue(service.isTradeDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate));
tradeDate = new GregorianCalendar(2017, 9, 23).getTime();
assertTrue(service.isTradeDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate));
tradeDate = new GregorianCalendar(2017, 9, 22, 0, 0, 1).getTime();
assertTrue(service.isTradeDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate));
tradeDate = new GregorianCalendar(2017, 9, 22).getTime();
assertFalse(service.isTradeDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate));
tradeDate = new GregorianCalendar(2017, 9, 21).getTime();
assertFalse(service.isTradeDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate));
}
@Test
public void testVerifyPubKeyHash() {
byte[] hashOfPubKey = Hash.getHash(Sig.getPublicKeyBytes(publicKey));
assertFalse(service.verifyPubKeyHash(new byte[0], publicKey));
assertFalse(service.verifyPubKeyHash(new byte[1], publicKey));
assertTrue(service.verifyPubKeyHash(hashOfPubKey, publicKey));
}
@Test
public void testVerifySignature() throws CryptoException {
byte[] ageWitnessInputData = "test".getBytes(Charset.forName("UTF-8"));
byte[] salt = "salt".getBytes(Charset.forName("UTF-8"));
final byte[] combined = ArrayUtils.addAll(ageWitnessInputData, salt);
byte[] hash = Sha256Hash.hash(combined);
byte[] signature = Sig.sign(keypair.getPrivate(), hash);
assertTrue(service.verifySignature(publicKey, hash, signature));
assertFalse(service.verifySignature(publicKey, new byte[0], new byte[0]));
assertFalse(service.verifySignature(publicKey, hash, "sig2".getBytes(Charset.forName("UTF-8"))));
assertFalse(service.verifySignature(publicKey, "hash2".getBytes(Charset.forName("UTF-8")), signature));
}
@Test
public void testVerifySignatureOfNonce() throws CryptoException {
int nonce = 1234;
byte[] nonceAsBytes = BigInteger.valueOf(nonce).toByteArray();
byte[] signature = Sig.sign(keypair.getPrivate(), nonceAsBytes);
assertTrue(service.verifySignatureOfNonce(publicKey, nonce, signature));
assertFalse(service.verifySignatureOfNonce(publicKey, nonce, "sig2".getBytes(Charset.forName("UTF-8"))));
assertFalse(service.verifySignatureOfNonce(publicKey, 0, new byte[0]));
assertFalse(service.verifySignatureOfNonce(publicKey, 9999, signature));
}
}