From baba4df3a979227d958fc28d711c3a9e1006bb0e Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Thu, 12 Nov 2015 16:06:58 +0200 Subject: [PATCH] Used API compatible with PCKS11 (e.g., smartcards); refactored --- .../java/meerkat/crypto/DigitalSignature.java | 57 +++-- .../crypto/concrete/ECDSASignature.java | 241 ++++++++++-------- .../src/main/java/meerkat/util/Hex.java | 26 ++ .../src/main/resources/logback.groovy | 2 +- .../crypto/concrete/TestECDSASignature.java | 99 ++++++- 5 files changed, 300 insertions(+), 125 deletions(-) create mode 100644 meerkat-common/src/main/java/meerkat/util/Hex.java diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index cc9151e..eda41a2 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -2,9 +2,12 @@ package meerkat.crypto; import com.google.protobuf.Message; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; import java.io.IOException; import java.io.InputStream; import java.security.InvalidKeyException; +import java.security.KeyStore; import java.security.SignatureException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; @@ -24,7 +27,7 @@ public interface DigitalSignature { * This will consume the entire stream. * Certificates can be either DER-encoded (binary) or PEM (base64) encoded. * This may be called multiple times to load several different certificates. - * It must be called before calling {@link #verify(Signature, List)}. + * It must be called before calling {@link #verify()}. * @param certStream source from which certificates are loaded * @throws CertificateException on parsing errors */ @@ -36,35 +39,55 @@ public interface DigitalSignature { */ public void clearVerificationCertificates(); + /** - * Verify a signature on a list of messages. - * The signature is computed on the serialized messages, separated with - * the {@link Digest#CONCAT_MARKER} string. - * For the signature to be valid, the verification key corresponding - * to the signature must have been loaded using {@link #loadVerificationCertificates(InputStream)}; - * otherwise a CertificateException is thrown + * Add msg to the content stream to be verified / signed. Each message is always (automatically) + * prepended with its length as a 32-bit unsigned integer in network byte order. * - * @param sig The signature on the messages - * @param msgs The messages themselves - * @return - * @throws CertificateException the certificate required for verification was not loaded. + * @param msg + * @throws SignatureException */ - public boolean verify(Signature sig, List msgs) + public void updateContent(Message msg) throws SignatureException; + + + /** + * Sign the content that was added. + * @return + * @throws SignatureException + */ + Signature sign() throws SignatureException; + + /** + * Initialize the verifier with the certificate whose Id is in sig. + * @param sig + * @throws CertificateException + * @throws InvalidKeyException + */ + void initVerify(Signature sig) throws CertificateException, InvalidKeyException; /** - * Loads a private signing key. The certificate must be in PKCS12 format and include both - * the public and private keys. + * Verify the updated content using the initialized signature. + * @return + */ + public boolean verify(); + + /** + * 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)} * Calling this method again will replace the key. * - * @param keyStream + * @param keyStoreBuilder A keystore builder that can be used to load a keystore. */ - public void loadSigningCertificate(InputStream keyStream, char[] password) + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) throws IOException, CertificateException, UnrecoverableKeyException; - public Signature sign(List msg) throws SignatureException; + + /** + * Clear the signing key (will require authentication to use again). + */ public void clearSigningKey(); } 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 e204aec..4fb8de5 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -1,19 +1,16 @@ package meerkat.crypto.concrete; -import java.io.IOError; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.security.*; +import java.security.cert.*; import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; import java.util.*; import com.google.protobuf.ByteString; import meerkat.crypto.Digest; import meerkat.protobuf.Crypto; +import meerkat.util.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,15 +19,18 @@ import com.google.protobuf.Message; import meerkat.crypto.DigitalSignature; import meerkat.protobuf.Crypto.Signature; -import org.bouncycastle.jce.provider.BouncyCastleProvider; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; /** * Sign and verify digital signatures. - *

+ *

* This class is not thread-safe (each thread should have its own instance). */ -public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignature { +public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignature { final Logger logger = LoggerFactory.getLogger(getClass()); final public static String KEYSTORE_TYPE = "PKCS12"; @@ -38,27 +38,39 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu SHA256Digest digest = new SHA256Digest(); - Map loadedCertificates = new HashMap<>(); + Map loadedCertificates = new HashMap<>(); + + /** + * Signature currently loaded (will be used in calls to {@link #verify()}). + */ + ByteString loadedSignature = null; ByteString loadedSigningKeyId = null; /** - * The actual signing implementation. + * The actual signing implementation. (used for both signing and verifying) */ java.security.Signature signer; /** - * Compute a fingerprint of a public key as a SHA256 hash. + * Compute a fingerprint of a cert as a SHA256 hash. * - * @param pubKey + * @param cert * @return */ - public ByteString computePublicKeyID(PublicKey pubKey) { - digest.reset(); - byte[] data = pubKey.getEncoded(); - digest.update(data); - return ByteString.copyFrom(digest.digest()); + public ByteString computeCertificateFingerprint(Certificate cert) { + try { + digest.reset(); + byte[] data = cert.getEncoded(); + digest.update(data); + return ByteString.copyFrom(digest.digest()); + } catch (CertificateEncodingException e) { + // Shouldn't happen + logger.error("Certificate encoding error", e); + return ByteString.EMPTY; + } + } public ECDSASignature(java.security.Signature signer) { @@ -70,7 +82,7 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu this.signer = java.security.Signature.getInstance(DEFAULT_SIGNATURE_ALGORITHM); } catch (NoSuchAlgorithmException e) { // Should never happen - logger.error("Couldn't find implementation for {} signatures: {}", DEFAULT_SIGNATURE_ALGORITHM, e); + logger.error("Couldn't find implementation for " + DEFAULT_SIGNATURE_ALGORITHM + " signatures", e); } } @@ -86,8 +98,8 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu continue; } PublicKey pubKey = cert.getPublicKey(); - ByteString keyId = computePublicKeyID(pubKey); - loadedCertificates.put(keyId, pubKey); + ByteString keyId = computeCertificateFingerprint(cert); + loadedCertificates.put(keyId, cert); } } @@ -97,17 +109,21 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu } - public void updateSigner(List msgs) throws SignatureException { - if (msgs == null) - return; - boolean first = true; - for (Message msg : msgs) { - if (!first) { - signer.update(Digest.CONCAT_MARKER); - } - first = false; - signer.update(msg.toByteString().asReadOnlyByteBuffer()); - } + /** + * Add the list of messages to the stream that is being verified/signed. + * Messages are separated with {@link Digest#CONCAT_MARKER} + * + * @param msg + * @throws SignatureException + */ + @Override + public void updateContent(Message msg) throws SignatureException { + assert msg != null; + int len = msg.getSerializedSize(); + + byte[] lenBytes = { (byte) ((len >>> 24) & 0xff), (byte) ((len >>> 16) & 0xff), (byte) ((len >>> 8) & 0xff), (byte) (len & 0xff) }; + signer.update(lenBytes); + signer.update(msg.toByteString().asReadOnlyByteBuffer()); } public void updateSigner(InputStream in) throws IOException, SignatureException { @@ -115,6 +131,7 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu signer.update(inStr.asReadOnlyByteBuffer()); } + @Override public Signature sign() throws SignatureException { Signature.Builder sig = Signature.newBuilder(); sig.setType(Crypto.SignatureType.ECDSA); @@ -123,105 +140,121 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu return sig.build(); } + @Override public void initVerify(Signature sig) - throws CertificateException, InvalidKeyException { - PublicKey pubKey = loadedCertificates.get(sig.getSignerId()); - if (pubKey == null) { - logger.warn("No public key loaded for ID {}!", sig.getSignerId()); - throw new CertificateException("No public key loaded for " + sig.getSignerId()); - } - signer.initVerify(pubKey); - } - - public boolean verify(Signature sig) { - try { - return signer.verify(sig.getData().toByteArray()); - } catch (SignatureException e) { - // Should never happen! - logger.error("Signature exception: {}", e); - return false; + throws CertificateException, InvalidKeyException { + Certificate cert = loadedCertificates.get(sig.getSignerId()); + if (cert == null) { + logger.warn("No certificate loaded for ID {}!", sig.getSignerId()); + throw new CertificateException("No certificate loaded for " + sig.getSignerId()); } + signer.initVerify(cert.getPublicKey()); + loadedSignature = sig.getData(); + loadedSigningKeyId = null; } @Override - public boolean verify(Signature sig, List msgs) - throws CertificateException { + public boolean verify() { try { - initVerify(sig); - updateSigner(msgs); - return verify(sig); - } catch (InvalidKeyException e) { - logger.error("Invalid key exception: {}", e); - return false; + return signer.verify(loadedSignature.toByteArray()); } catch (SignatureException e) { - // Should never happen! - logger.error("Signature exception: {}", e); + // Happens only if signature is invalid! + logger.error("Signature exception", e); return false; } } + /** + * Utility method to more easily deal with simple password-protected files. + * + * @param password + * @return + */ + public CallbackHandler getFixedPasswordHandler(final char[] password) { + return new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback) callback; + logger.debug("Requested password ({})", passwordCallback.getPrompt()); + passwordCallback.setPassword(password); + } + } + + } + }; + } + + /** + * Load a keystore from an input stream in PKCS12 format. + * + * @param keyStream + * @param password + * @return + * @throws IOException + * @throws CertificateException + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + */ + public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) + throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + keyStore.load(keyStream, password); + return KeyStore.Builder.newInstance(keyStore, new KeyStore.CallbackHandlerProtection(getFixedPasswordHandler(password))); + } + + + /** + * For now we only support PKCS12. + * TODO: Support for PKCS11 as well. + * + * @param keyStoreBuilder + * @throws IOException + * @throws CertificateException + * @throws UnrecoverableKeyException + */ @Override - public void loadSigningCertificate(InputStream keyStream, char[] password) + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) throws IOException, CertificateException, UnrecoverableKeyException { try { - KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); - keyStore.load(keyStream, password); - - KeyStore.ProtectionParameter protParam = - new KeyStore.PasswordProtection(password); + KeyStore keyStore = keyStoreBuilder.getKeyStore(); // Iterate through all aliases until we find the first privatekey Enumeration aliases = keyStore.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); - try { - KeyStore.Entry entry = keyStore.getEntry(alias, protParam); - if (entry instanceof KeyStore.PrivateKeyEntry) { - KeyStore.PrivateKeyEntry privEntry = (KeyStore.PrivateKeyEntry) entry; - PrivateKey privKey = privEntry.getPrivateKey(); - PublicKey pubKey = privEntry.getCertificate().getPublicKey(); - loadedSigningKeyId = computePublicKeyID(pubKey); - signer.initSign(privKey); - return; - } - } catch (InvalidKeyException e) { - logger.info("Read invalid key: {}", e); - } catch (UnrecoverableEntryException e) { - logger.info("Read unrecoverable entry: {}", e); - } + logger.trace("Testing keystore entry {}", alias); + try { + Certificate cert = keyStore.getCertificate(alias); + logger.trace("keystore entry {}, has cert type {}", alias, cert.getClass()); + Key key = keyStore.getKey(alias, null); + logger.trace("keystore entry {}, has key type {}", alias, key.getClass()); + if (key instanceof PrivateKey) { + loadedSigningKeyId = computeCertificateFingerprint(cert); + signer.initSign((PrivateKey) key); + 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()); + } + } catch(InvalidKeyException e) { + logger.info("Read invalid key", e); + } catch(UnrecoverableEntryException e) { + logger.info("Read unrecoverable entry", e); + } } + } catch (KeyStoreException e) { - logger.error("Keystore exception: {}", e); + logger.error("Keystore exception", e); } catch (NoSuchAlgorithmException e) { - logger.error("NoSuchAlgorithmException exception: {}", e); + logger.error("NoSuchAlgorithmException exception", e); throw new CertificateException(e); } - throw new UnrecoverableKeyException("Didn't find valid private key entry in stream!"); - } - - - @Override - public Signature sign(List msgs) { - try { - updateSigner(msgs); - return sign(); - } catch (SignatureException e) { - logger.error("Signature exception: {}", e); - return null; - } - } - - - public Signature sign(InputStream in) throws IOException { - try { - updateSigner(in); - return sign(); - } catch (SignatureException e) { - logger.error("Signature exception: {}", e); - return null; - } + logger.error("Didn't find valid private key entry in keystore"); + throw new UnrecoverableKeyException("Didn't find valid private key entry in keystore!"); } public void clearSigningKey() { diff --git a/meerkat-common/src/main/java/meerkat/util/Hex.java b/meerkat-common/src/main/java/meerkat/util/Hex.java new file mode 100644 index 0000000..2d0e4ef --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/util/Hex.java @@ -0,0 +1,26 @@ +package meerkat.util; + +import com.google.protobuf.ByteString; + +/** + * Convert to/from Hex + */ +public class Hex { + /** + * Encode a {@link ByteString} as a hex string. + * @param str + * @return + */ + public static String encode(ByteString str) { + StringBuilder s = new StringBuilder(); + for (byte b : str) { + s.append(Integer.toHexString(((int) b) & 0xff)); + } + return s.toString(); + } + + public static String encode(byte[] bytes) { + return encode(ByteString.copyFrom(bytes)); + } +} + diff --git a/meerkat-common/src/main/resources/logback.groovy b/meerkat-common/src/main/resources/logback.groovy index 1eb8d7d..076b6c4 100644 --- a/meerkat-common/src/main/resources/logback.groovy +++ b/meerkat-common/src/main/resources/logback.groovy @@ -20,7 +20,7 @@ def logOps = System.getProperty("log.ops") != null appender("CONSOLE", ConsoleAppender) { filter(ThresholdFilter) { - level = toLevel(System.getProperty("log.level"), DEBUG) + level = toLevel(System.getProperty("log.level"), TRACE) } encoder(PatternLayoutEncoder) { diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java index 99dbee1..4fbb35c 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java @@ -2,10 +2,16 @@ package meerkat.crypto.concrete; import com.google.protobuf.ByteString; import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.security.KeyStore; +import java.util.Arrays; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** @@ -21,15 +27,18 @@ public class TestECDSASignature { public static String MSG_PLAINTEXT_EXAMPLE = "/certs/signed-messages/helloworld.txt"; public static String MSG_SIG_EXAMPLE = "/certs/signed-messages/helloworld.txt.sha256sig"; + public static String HELLO_WORLD = "hello world!"; + @Test - public void testLoadSignatureKey() throws Exception { + public void loadSignatureKey() throws Exception { InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); char[] password = KEYFILE_PASSWORD.toCharArray(); ECDSASignature sig = new ECDSASignature(); - sig.loadSigningCertificate(keyStream, password); + KeyStore.Builder keyStore = sig.getPKCS12KeyStoreBuilder(keyStream, password); + sig.loadSigningCertificate(keyStore); keyStream.close(); } @@ -73,7 +82,91 @@ public class TestECDSASignature { Crypto.Signature builtSig = sig.build(); signer.initVerify(builtSig); signer.updateSigner(msgStream); - assertTrue("Signature did not verify!", signer.verify(builtSig)); + assertTrue("Signature did not verify!", signer.verify()); } + @Test + public void verifyInvalidSig() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); + InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); + InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); + + ECDSASignature signer = new ECDSASignature(); + + signer.loadVerificationCertificates(certStream); + certStream.close(); + + Crypto.Signature.Builder sig = Crypto.Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + sig.setSignerId(signer.loadedCertificates.entrySet().iterator().next().getKey()); + byte[] sigData = ByteString.readFrom(sigStream).toByteArray(); + ++sigData[0]; + + sig.setData(ByteString.copyFrom(sigData)); + + + Crypto.Signature builtSig = sig.build(); + signer.initVerify(builtSig); + signer.updateSigner(msgStream); + assertFalse("Bad Signature passed verification!", signer.verify()); + } + + + @Test + public void verifyInvalidMsg() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); + InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); + InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); + + ECDSASignature signer = new ECDSASignature(); + + signer.loadVerificationCertificates(certStream); + certStream.close(); + + Crypto.Signature.Builder sig = Crypto.Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + sig.setSignerId(signer.loadedCertificates.entrySet().iterator().next().getKey()); + sig.setData(ByteString.readFrom(sigStream)); + byte[] msgData = ByteString.readFrom(msgStream).toByteArray(); + ++msgData[0]; + + Crypto.Signature builtSig = sig.build(); + signer.initVerify(builtSig); + signer.updateSigner(msgStream); + assertFalse("Signature doesn't match message but passed verification!", signer.verify()); + } + + + + @Test + public void signAndVerify() 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); + + + Voting.UnsignedBulletinBoardMessage.Builder unsignedMsgBuilder = Voting.UnsignedBulletinBoardMessage.newBuilder(); + unsignedMsgBuilder.setData(ByteString.copyFromUtf8(HELLO_WORLD)); + unsignedMsgBuilder.addTags("Tag1"); + unsignedMsgBuilder.addTags("Tag2"); + unsignedMsgBuilder.addTags("Tag3"); + + Voting.UnsignedBulletinBoardMessage usMsg = unsignedMsgBuilder.build(); + + signer.updateContent(usMsg); + Crypto.Signature sig = signer.sign(); + + signer.loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + + signer.initVerify(sig); + signer.updateContent(usMsg); + assertTrue("Couldn't verify signature on ", signer.verify()); + } + + + }