diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index e0a9294..6234aca 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)}. * @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. 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/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/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/java/meerkat/crypto/concrete/ECDSASignatureTest.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java index 466c64c..e0337a9 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java @@ -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()); + } + + } + + }