Added deterministic version of signature generation (suitable for use in voting booth); improved signature tests
parent
984d7457c6
commit
9bcdb411e2
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue