diff --git a/bulletin-board-server/build.gradle b/bulletin-board-server/build.gradle index 2bf9d56..790f0d3 100644 --- a/bulletin-board-server/build.gradle +++ b/bulletin-board-server/build.gradle @@ -103,7 +103,6 @@ idea { def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" - println "Adding $srcDir" // add protobuf generated sources to generated source dir. if ("test".equals(sourceSet.name)) { testSourceDirs += file(srcDir) diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java index 3130560..8b51b65 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java @@ -17,7 +17,6 @@ import com.google.protobuf.ByteString; import meerkat.comm.CommunicationException; import meerkat.crypto.concrete.ECDSASignature; -import meerkat.crypto.concrete.TestECDSASignature; import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; import meerkat.protobuf.BulletinBoardAPI.FilterType; import meerkat.protobuf.BulletinBoardAPI.MessageFilter; @@ -48,13 +47,13 @@ public class GenericBulletinBoardServerTest { signers[0] = new ECDSASignature(); signers[1] = new ECDSASignature(); - InputStream keyStream = TestECDSASignature.class.getResourceAsStream(KEYFILE_EXAMPLE); + InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); char[] password = KEYFILE_PASSWORD.toCharArray(); KeyStore.Builder keyStore = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); signers[0].loadSigningCertificate(keyStore); - signers[0].loadVerificationCertificates(TestECDSASignature.class.getResourceAsStream(CERT1_PEM_EXAMPLE)); + signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); random = new SecureRandom(); } diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index d464a31..d2fe0fd 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -46,6 +46,7 @@ dependencies { compile 'com.google.protobuf:protobuf-java:3.+' // Crypto + compile 'org.factcenter.qilin:qilin:1.1+' compile 'org.bouncycastle:bcprov-jdk15on:1.53' compile 'org.bouncycastle:bcpkix-jdk15on:1.53' @@ -98,7 +99,6 @@ idea { def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" - println "Adding $srcDir" // add protobuf generated sources to generated source dir. if ("test".equals(sourceSet.name)) { testSourceDirs += file(srcDir) @@ -152,6 +152,8 @@ task fatCapsule(type: FatCapsule){ *===================================*/ repositories { + + mavenLocal(); // Prefer the local nexus repository (it may have 3rd party artifacts not found in mavenCentral) maven { diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index f13a886..587522f 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -51,7 +51,7 @@ public interface DigitalSignature { /** - * Sign the content that was added. + * Sign the content that was added using {@link #updateContent(Message)}. * Reset the DigitalSignature and make it available to sign a new message using the same key. * @return * @throws SignatureException @@ -76,7 +76,7 @@ public interface DigitalSignature { /** * Loads a private signing key. The keystore must include both the public and private * key parts. - * This method must be called before calling {@link #sign(List)} + * This method must be called before calling {@link #sign()} or {@link #updateContent(Message)} * Calling this method again will replace the key. * * @param keyStoreBuilder A keystore builder that can be used to load a keystore. diff --git a/meerkat-common/src/main/java/meerkat/crypto/Encryption.java b/meerkat-common/src/main/java/meerkat/crypto/Encryption.java index 0ec6c11..6a40d0d 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/Encryption.java +++ b/meerkat-common/src/main/java/meerkat/crypto/Encryption.java @@ -1,6 +1,11 @@ package meerkat.crypto; +import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; + +import java.io.IOException; +import java.util.Random; + import static meerkat.protobuf.Crypto.*; /** @@ -13,8 +18,23 @@ public interface Encryption { * @param rnd * @return */ - RerandomizableEncryptedMessage encrypt(Message plaintext, EncryptionRandomness rnd); // TODO: type of exception; throws + RerandomizableEncryptedMessage encrypt(Message plaintext, EncryptionRandomness rnd) throws IOException; // TODO: type of exception; throws - RerandomizableEncryptedMessage rerandomize(RerandomizableEncryptedMessage msg, EncryptionRandomness rnd); + /** + * Rerandomize a ciphertext using the supplied randomness. + * @param msg + * @param rnd + * @return + * @throws InvalidProtocolBufferException + */ + RerandomizableEncryptedMessage rerandomize(RerandomizableEncryptedMessage msg, EncryptionRandomness rnd) throws InvalidProtocolBufferException; + + /** + * Generate randomness compatible with {@link #encrypt(Message, EncryptionRandomness) and + * {@link #rerandomize(RerandomizableEncryptedMessage, EncryptionRandomness)}}. + * @param rand + * @return + */ + EncryptionRandomness generateRandomness(Random rand); } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSADeterministicSignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSADeterministicSignature.java new file mode 100644 index 0000000..992dbbb --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSADeterministicSignature.java @@ -0,0 +1,152 @@ +package meerkat.crypto.concrete; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Crypto.Signature; +import meerkat.util.Hex; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.signers.DSAKCalculator; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.jcajce.provider.asymmetric.util.DSAEncoder; +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.bouncycastle.jce.ECKeyUtil; +import org.bouncycastle.jce.ECPointUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.*; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECParameterSpec; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + + +/** + * Sign and verify digital signatures. + * + * Uses deterministic ECDSA signatures as per RFC 6979 + * + * This class uses BouncyCastle directly, so will not work with arbitrary PKCS11 providers. + * + * This class is not thread-safe (each thread should have its own instance). + */ +public class ECDSADeterministicSignature extends ECDSASignature { + final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Digest of message contents for deterministic signing. + */ + SHA256Digest msgDigest = new SHA256Digest(); + + /** + * The actual signing implementation. (used only signing -- superclass is used for verification) + */ + ECDSASigner deterministicSigner; + + /** + * Output the DER encoding of the ASN.1 sequence r,s + * @param r + * @param s + * @return + */ + public static byte[] derEncodeSignature(BigInteger r, BigInteger s) { + ASN1Integer[] rs = {new ASN1Integer(r), new ASN1Integer(s)}; + DERSequence seq = new DERSequence(rs); + + try { + return seq.getEncoded(); + } catch (IOException e) { + throw new RuntimeException("Should never happen! DER Encoding exception", e); + } + } + + public ECDSADeterministicSignature() { + DSAKCalculator kCalk = new HMacDSAKCalculator(new org.bouncycastle.crypto.digests.SHA256Digest()); + deterministicSigner = new ECDSASigner(kCalk); + } + + @Override + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) + throws CertificateException, UnrecoverableKeyException, IOException { + super.loadSigningCertificate(keyStoreBuilder); + + if (!(loadedSigningKey instanceof ECPrivateKey)) { + logger.error("Wrong private key type (expected ECPrivateKey, got {})", loadedSigningKey.getClass()); + throw new CertificateException("Wrong signing key type!"); + } + ECPrivateKey key = (ECPrivateKey) loadedSigningKey; + + AsymmetricKeyParameter baseParams; + try { + baseParams = ECUtil.generatePrivateKeyParameter(key); + } catch (InvalidKeyException e) { + throw new UnrecoverableKeyException("Couldn't convert private key"); + } + + if (!(baseParams instanceof ECPrivateKeyParameters)) { + logger.error("Error converting to bouncycastle type! (got {})", baseParams.getClass()); + throw new UnrecoverableKeyException("Wrong signing key type!"); + } + + ECPrivateKeyParameters params = (ECPrivateKeyParameters) baseParams; + + deterministicSigner.init(true, params); + } + + /** + * Add the list of messages to the stream that is being verified/signed. + * Messages are prepended with their length in 32-bit big-endian format. + * + * @param msg + * @throws SignatureException + */ + @Override + public void updateContent(Message msg) throws SignatureException { + assert msg != null; + + // We're doing twice the digest work so that we also support verification with the same update. + // If this becomes a problem, we can decide which way to update based on which init was called. + super.updateContent(msg); + msgDigest.update(msg); + } + + @Override + public void updateContent(InputStream in) throws IOException, SignatureException { + ByteString inStr = ByteString.readFrom(in); + signer.update(inStr.asReadOnlyByteBuffer()); + msgDigest.update(inStr); + } + + @Override + public Signature sign() throws SignatureException { + Signature.Builder sig = Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + + BigInteger[] rawSig = deterministicSigner.generateSignature(msgDigest.digest()); + + sig.setData(ByteString.copyFrom(derEncodeSignature(rawSig[0], rawSig[1]))); + + sig.setSignerId(loadedSigningKeyId); + return sig.build(); + } +} diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java index 1a5683f..6cbcf8d 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -36,7 +36,7 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur final public static String KEYSTORE_TYPE = "PKCS12"; final public static String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withECDSA"; - SHA256Digest digest = new SHA256Digest(); + SHA256Digest certDigest = new SHA256Digest(); /** * Buffer used to hold length in for hash update @@ -58,6 +58,11 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur */ java.security.Signature signer; + /** + * The currently loaded signing key. + */ + PrivateKey loadedSigningKey; + /** * Compute a fingerprint of a cert as a SHA256 hash. @@ -67,14 +72,14 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur */ public ByteString computeCertificateFingerprint(Certificate cert) { try { - digest.reset(); + certDigest.reset(); byte[] data = cert.getEncoded(); - digest.update(data); - return ByteString.copyFrom(digest.digest()); + certDigest.update(data); + return ByteString.copyFrom(certDigest.digest()); } catch (CertificateEncodingException e) { // Shouldn't happen logger.error("Certificate encoding error", e); - return ByteString.EMPTY; + throw new RuntimeException("Certificate encoding error", e); } } @@ -133,7 +138,7 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur signer.update(msg.toByteString().asReadOnlyByteBuffer()); } - public void updateSigner(InputStream in) throws IOException, SignatureException { + public void updateContent(InputStream in) throws IOException, SignatureException { ByteString inStr = ByteString.readFrom(in); signer.update(inStr.asReadOnlyByteBuffer()); } @@ -234,10 +239,13 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur String alias = aliases.nextElement(); logger.trace("Testing keystore entry {}", alias); + try { Certificate cert = keyStore.getCertificate(alias); logger.trace("keystore entry {}, has cert type {}", alias, cert.getClass()); + Key key; + try { key = keyStore.getKey(alias, null); } catch (UnrecoverableKeyException e) { @@ -267,9 +275,11 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur } logger.trace("keystore entry {}, has key type {}", alias, key.getClass()); if (key instanceof PrivateKey) { + loadedSigningKey = (PrivateKey) key; loadedSigningKeyId = computeCertificateFingerprint(cert); - signer.initSign((PrivateKey) key); + signer.initSign(loadedSigningKey); logger.debug("Loaded signing key with ID {}", Hex.encode(loadedSigningKeyId)); + return; } else { logger.info("Certificate {} in keystore does not have a private key", cert.toString()); @@ -297,6 +307,8 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur if (loadedSigningKeyId != null) signer.initSign(null); loadedSigningKeyId = null; + loadedSigningKey = null; + // Start garbage collection? } catch (InvalidKeyException e) { // Do nothing } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java new file mode 100644 index 0000000..8718dd7 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java @@ -0,0 +1,140 @@ +package meerkat.crypto.concrete; + +import com.google.protobuf.ByteString; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import meerkat.crypto.Encryption; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECFieldElement; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.util.BigIntegers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qilin.primitives.concrete.ECElGamal; +import qilin.primitives.concrete.ECGroup; +import qilin.primitives.PseudorandomGenerator; +import qilin.primitives.generic.ElGamal; +import qilin.util.PRGRandom; +import qilin.util.Pair; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.security.spec.*; +import java.util.Random; + +/** + * Created by talm on 17/11/15. + */ +public class ECElGamalEncryption extends GlobalCryptoSetup implements Encryption { + final Logger logger = LoggerFactory.getLogger(getClass()); + + public final static String KEY_ALGORITHM = "ECDH"; + + /** + * The Qilin format El-Gamal public key + */ + ECElGamal.PK elGamalPK; + + ECCurve curve; + + ECGroup group; + + public ECGroup getGroup() { return group; } + + public ECElGamal.PK getElGamalPK() { + return elGamalPK; + } + + public void init(ConcreteCrypto.ElGamalPublicKey serializedPk) throws InvalidKeySpecException { + AsymmetricKeyParameter keyParam; + + try { + keyParam = PublicKeyFactory.createKey(serializedPk.getSubjectPublicKeyInfo().toByteArray()); + } catch (IOException e) { + // Shouldn't every happen + logger.error("Invalid Public Key Encoding", e); + throw new InvalidKeySpecException("Invalid Public Key Encoding", e); + } + + if (!(keyParam instanceof ECPublicKeyParameters)) { + logger.error("Public key is a {}, not a valid public EC Key!", keyParam.getClass()); + throw new InvalidKeySpecException("Not a valid EC public key!"); + } + + ECDomainParameters params = ((ECKeyParameters) keyParam).getParameters(); + ECParameterSpec ecParams = new ECParameterSpec(params.getCurve(), params.getG(), params.getN(), params.getH(), + params.getSeed()); + + curve = params.getCurve(); + group = new ECGroup(ecParams); + + elGamalPK = new ECElGamal.PK(group, ((ECPublicKeyParameters) keyParam).getQ()); + } + + + @Override + public Crypto.RerandomizableEncryptedMessage encrypt(Message plaintext, Crypto.EncryptionRandomness rnd) { + + // We write the message using writeDelimited to so the length gets prepended. + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + plaintext.writeDelimitedTo(out); + } catch (IOException e) { + logger.error("Should never happen!", e); + throw new RuntimeException("Error in ByteArrayOutputStream!", e); + } + byte[] msg = out.toByteArray(); + ECPoint encodedMsg = group.injectiveEncode(msg, new PRGRandom(msg)); + + BigInteger rndInt = BigIntegers.fromUnsignedByteArray(rnd.getData().toByteArray()); + Pair cipherText = elGamalPK.encrypt(encodedMsg, rndInt); + ConcreteCrypto.ElGamalCiphertext encodedCipherText = ConcreteCrypto.ElGamalCiphertext.newBuilder() + .setC1(ByteString.copyFrom(cipherText.a.getEncoded(true))) + .setC2(ByteString.copyFrom(cipherText.b.getEncoded(true))) + .build(); + + return Crypto.RerandomizableEncryptedMessage.newBuilder() + .setData(encodedCipherText.toByteString()) + .build(); + } + + @Override + public Crypto.RerandomizableEncryptedMessage rerandomize(Crypto.RerandomizableEncryptedMessage msg, Crypto.EncryptionRandomness rnd) throws InvalidProtocolBufferException { + BigInteger rndInt = BigIntegers.fromUnsignedByteArray(rnd.getData().toByteArray()); + Pair randomizer = elGamalPK.encrypt(curve.getInfinity(), rndInt); + ConcreteCrypto.ElGamalCiphertext originalEncodedCipher= ConcreteCrypto.ElGamalCiphertext.parseFrom(msg.getData()); + + Pair originalCipher = new Pair<>( + curve.decodePoint(originalEncodedCipher.getC1().toByteArray()), + curve.decodePoint(originalEncodedCipher.getC2().toByteArray())); + Pair newCipher = elGamalPK.add(originalCipher, randomizer); + + return Crypto.RerandomizableEncryptedMessage.newBuilder() + .setData( + ConcreteCrypto.ElGamalCiphertext.newBuilder() + .setC1(ByteString.copyFrom(newCipher.a.getEncoded(true))) + .setC2(ByteString.copyFrom(newCipher.b.getEncoded(true))) + .build().toByteString() + ).build(); + } + + @Override + public Crypto.EncryptionRandomness generateRandomness(Random rand) { + BigInteger randomInt = new BigInteger(group.getCurveParams().getN().bitLength() - 1, rand); + Crypto.EncryptionRandomness retval = Crypto.EncryptionRandomness.newBuilder() + .setData(ByteString.copyFrom(BigIntegers.asUnsignedByteArray(randomInt))).build(); + + return retval; + } +} diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java index 764fd8d..4f2e7a5 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java @@ -4,6 +4,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.Provider; import java.security.Security; /** @@ -13,6 +14,7 @@ public class GlobalCryptoSetup { final static Logger logger = LoggerFactory.getLogger(GlobalCryptoSetup.class); static boolean loadedBouncyCastle = false; + static Provider bouncyCastleProvider; public static boolean hasSecp256k1Curve() { // For now we just check if the java version is at least 8 @@ -22,17 +24,20 @@ public class GlobalCryptoSetup { return ((major > 1) || ((major > 0) && (minor > 7))); } - public static void doSetup() { - // Make bouncycastle our default provider if we're running on a JVM version < 8 - // (earlier version don't support the EC curve we use for signatures) - if (!hasSecp256k1Curve() && !loadedBouncyCastle) { - loadedBouncyCastle = true; - Security.insertProviderAt(new BouncyCastleProvider(), 1); - logger.info("Using BouncyCastle instead of native provider to support secp256k1 named curve"); + public static Provider getBouncyCastleProvider() { doSetup(); return bouncyCastleProvider; } + + public static synchronized void doSetup() { + if (bouncyCastleProvider == null) { + bouncyCastleProvider = new BouncyCastleProvider(); + // Make bouncycastle our default provider if we're running on a JVM version < 8 + // (earlier version don't support the EC curve we use for signatures) + if (!hasSecp256k1Curve() && !loadedBouncyCastle) { + loadedBouncyCastle = true; + Security.insertProviderAt(bouncyCastleProvider, 1); + logger.info("Using BouncyCastle instead of native provider to support secp256k1 named curve"); + } } } - public GlobalCryptoSetup() { - doSetup(); - } + public GlobalCryptoSetup() { doSetup(); } } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java index d18a4d1..4f60af3 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java @@ -78,6 +78,10 @@ public class SHA256Digest extends GlobalCryptoSetup implements Digest { hash.update(msg); } + final public void update(ByteBuffer msg) { + hash.update(msg); + } + @Override public void reset() { hash.reset(); diff --git a/meerkat-common/src/main/proto/meerkat/concrete_crypto.proto b/meerkat-common/src/main/proto/meerkat/concrete_crypto.proto new file mode 100644 index 0000000..d8c40d3 --- /dev/null +++ b/meerkat-common/src/main/proto/meerkat/concrete_crypto.proto @@ -0,0 +1,22 @@ +// Protobufs for specific crypto primitives + +syntax = "proto3"; + +package meerkat; + +import 'meerkat/crypto.proto'; + +option java_package = "meerkat.protobuf"; + + +message ElGamalPublicKey { + // DER-encoded SubjectPublicKeyInfo as in RFC 3279 + bytes subject_public_key_info = 1; +} + +// An El-Gamal ciphertext +// Each group element should be an ASN.1 encoded curve point with compression. +message ElGamalCiphertext { + bytes c1 = 1; // First group element + bytes c2 = 2; // Second group element +} \ No newline at end of file diff --git a/meerkat-common/src/main/proto/meerkat/crypto.proto b/meerkat-common/src/main/proto/meerkat/crypto.proto index ecd9478..eeec159 100644 --- a/meerkat-common/src/main/proto/meerkat/crypto.proto +++ b/meerkat-common/src/main/proto/meerkat/crypto.proto @@ -9,6 +9,10 @@ enum SignatureType { DSA = 1; } +message BigInteger { + bytes data = 1; +} + // A digital signature message Signature { SignatureType type = 1; diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSADeterministicSignatureTest.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSADeterministicSignatureTest.java new file mode 100644 index 0000000..95b9d3e --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSADeterministicSignatureTest.java @@ -0,0 +1,52 @@ +package meerkat.crypto.concrete; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.protobuf.Crypto; +import org.junit.Test; + +import java.io.InputStream; +import java.math.BigInteger; +import java.security.KeyStore; + +import static org.junit.Assert.*; + +/** + * Created by talm on 12/11/15. + */ +public class ECDSADeterministicSignatureTest extends ECDSASignatureTest { + + @Override + protected ECDSASignature getSigner() { return new ECDSADeterministicSignature(); } + + + /** + * Make sure signatures don't vary + */ + @Test + public void testDeterministicSigning() throws Exception { + loadSigningKeys(); + + + for (int i = 0; i < REPEAT_COUNT; ++i) { + BigInteger rawMsg = new BigInteger(50, rand); + Message msg = Crypto.BigInteger.newBuilder() + .setData(ByteString.copyFrom(rawMsg.toByteArray())).build(); + Crypto.Signature[] sigs = new Crypto.Signature[REPEAT_COUNT]; + + signer.updateContent(msg); + sigs[0] = signer.sign(); + byte[] canonicalSig = sigs[0].toByteArray(); + + for (int j = 1; j < sigs.length; ++j) { + signer.updateContent(msg); + sigs[j] = signer.sign(); + + byte[] newSig = sigs[j].toByteArray(); + + assertArrayEquals("Signatures on same message differ (i="+i+",j="+j+")", canonicalSig, newSig); + } + } + } +} diff --git a/meerkat-common/src/test/meerkat/crypto/concrete/TestECDSASignature.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java similarity index 64% rename from meerkat-common/src/test/meerkat/crypto/concrete/TestECDSASignature.java rename to meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java index a3a0ec7..e0337a9 100644 --- a/meerkat-common/src/test/meerkat/crypto/concrete/TestECDSASignature.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java @@ -1,13 +1,16 @@ package meerkat.crypto.concrete; import com.google.protobuf.ByteString; +import com.google.protobuf.Message; import meerkat.protobuf.Crypto; -import meerkat.crypto.concrete.ECDSASignature; import meerkat.protobuf.BulletinBoardAPI.*; +import org.junit.Before; import org.junit.Test; import java.io.InputStream; +import java.math.BigInteger; import java.security.KeyStore; +import java.util.Random; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -15,7 +18,7 @@ import static org.junit.Assert.assertTrue; /** * Created by talm on 12/11/15. */ -public class TestECDSASignature { +public class ECDSASignatureTest { public static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; public static String KEYFILE_PASSWORD = "secret"; @@ -27,16 +30,26 @@ public class TestECDSASignature { public static String HELLO_WORLD = "hello world!"; + public final static int REPEAT_COUNT = 10; + + Random rand = new Random(0); + + protected ECDSASignature signer; + + protected ECDSASignature getSigner() { return new ECDSASignature(); } + + @Before + public void setup() throws Exception { + signer = getSigner(); + } @Test public void loadSignatureKey() throws Exception { InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); char[] password = KEYFILE_PASSWORD.toCharArray(); - ECDSASignature sig = new ECDSASignature(); - - KeyStore.Builder keyStore = sig.getPKCS12KeyStoreBuilder(keyStream, password); - sig.loadSigningCertificate(keyStore); + KeyStore.Builder keyStore = signer.getPKCS12KeyStoreBuilder(keyStream, password); + signer.loadSigningCertificate(keyStore); keyStream.close(); } @@ -44,9 +57,7 @@ public class TestECDSASignature { public void loadPEMVerificationKey() throws Exception { InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); - ECDSASignature sig = new ECDSASignature(); - - sig.loadVerificationCertificates(certStream); + signer.loadVerificationCertificates(certStream); certStream.close(); } @@ -54,9 +65,7 @@ public class TestECDSASignature { public void loadDERVerificationKey() throws Exception { InputStream certStream = getClass().getResourceAsStream(CERT2_DER_EXAMPLE); - ECDSASignature sig = new ECDSASignature(); - - sig.loadVerificationCertificates(certStream); + signer.loadVerificationCertificates(certStream); certStream.close(); } @@ -67,8 +76,6 @@ public class TestECDSASignature { InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); - ECDSASignature signer = new ECDSASignature(); - signer.loadVerificationCertificates(certStream); certStream.close(); @@ -79,7 +86,7 @@ public class TestECDSASignature { Crypto.Signature builtSig = sig.build(); signer.initVerify(builtSig); - signer.updateSigner(msgStream); + signer.updateContent(msgStream); assertTrue("Signature did not verify!", signer.verify()); } @@ -89,8 +96,6 @@ public class TestECDSASignature { InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); - ECDSASignature signer = new ECDSASignature(); - signer.loadVerificationCertificates(certStream); certStream.close(); @@ -105,7 +110,7 @@ public class TestECDSASignature { Crypto.Signature builtSig = sig.build(); signer.initVerify(builtSig); - signer.updateSigner(msgStream); + signer.updateContent(msgStream); assertFalse("Bad Signature passed verification!", signer.verify()); } @@ -116,8 +121,6 @@ public class TestECDSASignature { InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); - ECDSASignature signer = new ECDSASignature(); - signer.loadVerificationCertificates(certStream); certStream.close(); @@ -130,30 +133,27 @@ public class TestECDSASignature { Crypto.Signature builtSig = sig.build(); signer.initVerify(builtSig); - signer.updateSigner(msgStream); + signer.updateContent(msgStream); assertFalse("Signature doesn't match message but passed verification!", signer.verify()); } - @Test - public void signAndVerify() throws Exception { + protected void loadSigningKeys() throws Exception { InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); char[] password = KEYFILE_PASSWORD.toCharArray(); - ECDSASignature signer = new ECDSASignature(); - KeyStore.Builder keyStore = signer.getPKCS12KeyStoreBuilder(keyStream, password); signer.loadSigningCertificate(keyStore); + } + @Test + public void signAndVerify() throws Exception { + loadSigningKeys(); - UnsignedBulletinBoardMessage.Builder unsignedMsgBuilder = UnsignedBulletinBoardMessage.newBuilder(); - unsignedMsgBuilder.setData(ByteString.copyFromUtf8(HELLO_WORLD)); - unsignedMsgBuilder.addTag("Tag1"); - unsignedMsgBuilder.addTag("Tag2"); - unsignedMsgBuilder.addTag("Tag3"); - - UnsignedBulletinBoardMessage usMsg = unsignedMsgBuilder.build(); + BigInteger rawMsg = new BigInteger(50, rand); + Crypto.BigInteger usMsg = Crypto.BigInteger.newBuilder() + .setData(ByteString.copyFrom(rawMsg.toByteArray())).build(); signer.updateContent(usMsg); Crypto.Signature sig = signer.sign(); @@ -166,5 +166,55 @@ public class TestECDSASignature { } + @Test + public void signMultipleAndVerify() throws Exception { + loadSigningKeys(); + + Message[] msgs = new Message[REPEAT_COUNT]; + for (int i = 0; i < msgs.length; ++i) { + + BigInteger rawMsg = new BigInteger(50, rand); + msgs[i] = Crypto.BigInteger.newBuilder() + .setData(ByteString.copyFrom(rawMsg.toByteArray())).build(); + signer.updateContent(msgs[i]); + } + + Crypto.Signature sig = signer.sign(); + + signer.loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + + signer.initVerify(sig); + for (int i = 0; i < msgs.length; ++i) { + signer.updateContent(msgs[i]); + } + assertTrue("Couldn't verify signature on ", signer.verify()); + } + + @Test + public void multipleSignAndVerify() throws Exception { + loadSigningKeys(); + + Message[] msgs = new Message[REPEAT_COUNT]; + Crypto.Signature[] sigs = new Crypto.Signature[REPEAT_COUNT]; + for (int i = 0; i < msgs.length; ++i) { + BigInteger rawMsg = new BigInteger(50, rand); + msgs[i] = Crypto.BigInteger.newBuilder() + .setData(ByteString.copyFrom(rawMsg.toByteArray())).build(); + signer.updateContent(msgs[i]); + sigs[i] = signer.sign(); + } + + signer.loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + + + for (int i = 0; i < msgs.length; ++i) { + signer.initVerify(sigs[i]); + signer.updateContent(msgs[i]); + assertTrue("Couldn't verify signature on ", signer.verify()); + } + + } + + } diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalEncryptionTest.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalEncryptionTest.java new file mode 100644 index 0000000..0d628e8 --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalEncryptionTest.java @@ -0,0 +1,125 @@ +package meerkat.crypto.concrete; + +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; +import org.bouncycastle.math.ec.ECPoint; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qilin.primitives.concrete.ECElGamal; +import qilin.primitives.concrete.ECGroup; +import qilin.primitives.generic.ElGamal; +import qilin.util.Pair; + +import java.math.BigInteger; +import java.util.Random; + +import static org.junit.Assert.*; + +/** + * Test class for {@link ECElGamalEncryption} + */ +public class ECElGamalEncryptionTest { + final Logger logger = LoggerFactory.getLogger(getClass()); + /** + * Number of times to repeat probabilistic tests. + */ + public final static int CONFIDENCE = 10; + + Random rand = new Random(0); // Insecure deterministic random for testing. + + ECElGamal.SK key; + ECGroup group; + ECElGamalEncryption enc; + ConcreteCrypto.ElGamalPublicKey serializedPk; + + + @Before + public void setup() throws Exception { + group = new ECGroup("secp256k1"); + BigInteger sk = ECElGamal.generateSecretKey(group, rand); + key = new ECElGamal.SK(group, sk); + serializedPk = ECElGamalUtils.serializePk(group, key); + + + enc = new ECElGamalEncryption(); + + enc.init(serializedPk); + } + + + Voting.PlaintextBallot genRandomBallot(int numQuestions, int numAnswers, int maxAnswer) { + Voting.PlaintextBallot.Builder ballot = Voting.PlaintextBallot.newBuilder(); + ballot.setSerialNumber(rand.nextInt(1000000)); + for (int i = 0; i < numQuestions; ++i) { + Voting.BallotAnswer.Builder answers = ballot.addAnswersBuilder(); + for (int j = 0; j < numAnswers; ++j) { + answers.addAnswer(rand.nextInt(maxAnswer)); + } + } + return ballot.build(); + } + + /** + * Testing just the key management + * @throws Exception + */ + @Test + public void testPkSerialization() throws Exception { + ECElGamal.PK pk = enc.getElGamalPK(); + + ECPoint point = enc.getGroup().sample(rand); + Pair cipher = pk.encrypt(point, pk.getRandom(rand)); + + ECPoint decrypted = key.decrypt(cipher); + + assertEquals("Decrypted value not equal to encrypted value!", point, decrypted); + } + + @Test + public void testEncryption() throws Exception { + for (int i = 0; i < CONFIDENCE; ++i) { + Voting.PlaintextBallot msg = genRandomBallot(2,3,16); // 2 questions with 3 answers each, in range 0-15. + if (msg.getSerializedSize() > enc.getGroup().getInjectiveEncodeMsgLength()) { + logger.error("Test Message too big (|msg|={} > max={}), expect failure.", + msg.getSerializedSize(), enc.getGroup().getInjectiveEncodeMsgLength()); + } + + Crypto.RerandomizableEncryptedMessage cipherText = enc.encrypt(msg, enc.generateRandomness(rand)); + + Voting.PlaintextBallot decrypted = ECElGamalUtils.decrypt(Voting.PlaintextBallot.class, key, group, cipherText); + + assertEquals("Decrypted value differs from encrypted value (i="+i+")!", msg, decrypted); + } + } + + @Test + public void testRerandomizeModifiesCiphertext() throws Exception { + Voting.PlaintextBallot msg = genRandomBallot(2,3,16); // 2 questions with 3 answers each, in range 0-15. + Crypto.RerandomizableEncryptedMessage cipher1 = enc.encrypt(msg, enc.generateRandomness(rand)); + Crypto.RerandomizableEncryptedMessage cipher2 = enc.rerandomize(cipher1, enc.generateRandomness(rand)); + assertNotEquals("Rerandomized cipher identical to original!", cipher1, cipher2); + } + + @Test + public void testRerandomizePreservesPlaintext() throws Exception { + for (int i = 0; i < CONFIDENCE; ++i) { + Voting.PlaintextBallot msg = genRandomBallot(2,3,16); // 2 questions with 3 answers each, in range 0-15. + + Crypto.RerandomizableEncryptedMessage cipher = enc.encrypt(msg, enc.generateRandomness(rand)); + Crypto.RerandomizableEncryptedMessage cipher2 = cipher; + for (int j = 0; j < CONFIDENCE; ++j) + cipher2 = enc.rerandomize(cipher2, enc.generateRandomness(rand)); + + Voting.PlaintextBallot decrypted = ECElGamalUtils.decrypt(Voting.PlaintextBallot.class, key, group, + cipher2); + + assertEquals("Decrypted value differs from original encrypted value (i="+i+")!", msg, decrypted); + } + } +} + diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java new file mode 100644 index 0000000..fec4803 --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java @@ -0,0 +1,89 @@ +package meerkat.crypto.concrete; + +import com.google.protobuf.ByteString; +import com.google.protobuf.GeneratedMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import meerkat.protobuf.ConcreteCrypto; +import meerkat.protobuf.Crypto; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qilin.primitives.concrete.ECElGamal; +import qilin.primitives.concrete.ECGroup; +import qilin.primitives.generic.ElGamal; +import qilin.util.Pair; + +import java.io.ByteArrayInputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; + +/** + * utilities for ECElgamal + */ +public class ECElGamalUtils { + final static Logger logger = LoggerFactory.getLogger(ECElGamalUtils.class); + + public final static String ENCRYPTION_KEY_ALGORITHM = "ECDH"; + + /** + * Serialize an El-Gamal public key into a form acceptable by {@link ECElGamalEncryption} + * @param pk + * @return + */ + public static ConcreteCrypto.ElGamalPublicKey serializePk(ECGroup group, ElGamal.PK pk) { + ECPoint pkPoint = pk.getPK(); + ECParameterSpec params = group.getCurveParams(); + + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(pkPoint, params); + + try { + KeyFactory fact = KeyFactory.getInstance(ENCRYPTION_KEY_ALGORITHM, + GlobalCryptoSetup.getBouncyCastleProvider()); + PublicKey javaPk = fact.generatePublic(pubKeySpec); + ConcreteCrypto.ElGamalPublicKey serializedPk = ConcreteCrypto.ElGamalPublicKey.newBuilder() + .setSubjectPublicKeyInfo(ByteString.copyFrom(javaPk.getEncoded())).build(); + + return serializedPk; + } catch (NoSuchAlgorithmException|InvalidKeySpecException e) { + logger.error("Should never happen!", e); + throw new RuntimeException("Error converting public key!", e); + } + } + + /** + * Standard (non-threshold) decryption for testing purposes. + * @param secretKey + * @return + */ + public static T decrypt(Class plaintextMessageType, ECElGamal.SK secretKey, ECGroup group, Crypto.RerandomizableEncryptedMessage opaqueCipher) + throws InvalidProtocolBufferException { + ConcreteCrypto.ElGamalCiphertext cipherText = ConcreteCrypto.ElGamalCiphertext.parseFrom(opaqueCipher.getData()); + ByteString c1encoded = cipherText.getC1(); + ByteString c2encoded = cipherText.getC2(); + + ECPoint c1 = group.decode(c1encoded.toByteArray()); + ECPoint c2 = group.decode(c2encoded.toByteArray()); + + ECPoint plaintextEncoded = secretKey.decrypt(new Pair(c1, c2)); + + byte[] plaintext = group.injectiveDecode(plaintextEncoded); + + ByteArrayInputStream in = new ByteArrayInputStream(plaintext); + + try { + java.lang.reflect.Method newBuilder = plaintextMessageType.getMethod("newBuilder"); + GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(plaintextMessageType); + builder.mergeDelimitedFrom(in); + return plaintextMessageType.cast(builder.build()); + } catch (Exception e) { + logger.error("Error parsing incoming message", e); + throw new InvalidProtocolBufferException("Plaintext protobuf error"); + } + } + +} diff --git a/polling-station/build.gradle b/polling-station/build.gradle index a0c7e72..34fe58d 100644 --- a/polling-station/build.gradle +++ b/polling-station/build.gradle @@ -73,7 +73,6 @@ idea { def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" - println "Adding $srcDir" // add protobuf generated sources to generated source dir. if ("test".equals(sourceSet.name)) { testSourceDirs += file(srcDir) diff --git a/restful-api-common/build.gradle b/restful-api-common/build.gradle index 1dea7a3..3c0ad1e 100644 --- a/restful-api-common/build.gradle +++ b/restful-api-common/build.gradle @@ -73,7 +73,6 @@ idea { def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" - println "Adding $srcDir" // add protobuf generated sources to generated source dir. if ("test".equals(sourceSet.name)) { testSourceDirs += file(srcDir) diff --git a/voting-booth/build.gradle b/voting-booth/build.gradle index a52e1f6..70d7340 100644 --- a/voting-booth/build.gradle +++ b/voting-booth/build.gradle @@ -72,7 +72,6 @@ idea { def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" - println "Adding $srcDir" // add protobuf generated sources to generated source dir. if ("test".equals(sourceSet.name)) { testSourceDirs += file(srcDir)