Added deterministic version of signature generation (suitable for use in voting booth); improved signature tests

crypto-primitives
Tal Moran 2015-11-23 02:06:53 +02:00
parent 984d7457c6
commit 9bcdb411e2
6 changed files with 310 additions and 42 deletions

View File

@ -51,7 +51,7 @@ public interface DigitalSignature {
/**
* Sign the content that was added.
* Sign the content that was added using {@link #updateContent(Message)}.
* @return
* @throws SignatureException
*/
@ -75,7 +75,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.

View File

@ -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();
}
}

View File

@ -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
}

View File

@ -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();

View File

@ -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);
}
}
}
}

View File

@ -1,15 +1,16 @@
package meerkat.crypto.concrete;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import meerkat.protobuf.Crypto;
import meerkat.protobuf.BulletinBoardAPI.*;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Random;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -29,16 +30,26 @@ public class ECDSASignatureTest {
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();
}
@ -46,9 +57,7 @@ public class ECDSASignatureTest {
public void loadPEMVerificationKey() throws Exception {
InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE);
ECDSASignature sig = new ECDSASignature();
sig.loadVerificationCertificates(certStream);
signer.loadVerificationCertificates(certStream);
certStream.close();
}
@ -56,9 +65,7 @@ public class ECDSASignatureTest {
public void loadDERVerificationKey() throws Exception {
InputStream certStream = getClass().getResourceAsStream(CERT2_DER_EXAMPLE);
ECDSASignature sig = new ECDSASignature();
sig.loadVerificationCertificates(certStream);
signer.loadVerificationCertificates(certStream);
certStream.close();
}
@ -69,8 +76,6 @@ public class ECDSASignatureTest {
InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE);
InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE);
ECDSASignature signer = new ECDSASignature();
signer.loadVerificationCertificates(certStream);
certStream.close();
@ -81,7 +86,7 @@ public class ECDSASignatureTest {
Crypto.Signature builtSig = sig.build();
signer.initVerify(builtSig);
signer.updateSigner(msgStream);
signer.updateContent(msgStream);
assertTrue("Signature did not verify!", signer.verify());
}
@ -91,8 +96,6 @@ public class ECDSASignatureTest {
InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE);
InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE);
ECDSASignature signer = new ECDSASignature();
signer.loadVerificationCertificates(certStream);
certStream.close();
@ -107,7 +110,7 @@ public class ECDSASignatureTest {
Crypto.Signature builtSig = sig.build();
signer.initVerify(builtSig);
signer.updateSigner(msgStream);
signer.updateContent(msgStream);
assertFalse("Bad Signature passed verification!", signer.verify());
}
@ -118,8 +121,6 @@ public class ECDSASignatureTest {
InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE);
InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE);
ECDSASignature signer = new ECDSASignature();
signer.loadVerificationCertificates(certStream);
certStream.close();
@ -132,30 +133,27 @@ public class ECDSASignatureTest {
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.addTags("Tag1");
unsignedMsgBuilder.addTags("Tag2");
unsignedMsgBuilder.addTags("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();
@ -168,5 +166,55 @@ public class ECDSASignatureTest {
}
@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());
}
}
}