Used API compatible with PCKS11 (e.g., smartcards); refactored

Bulletin_Board_Server_phase_1
Tal Moran 2015-11-12 16:06:58 +02:00
parent b839447f87
commit baba4df3a9
5 changed files with 300 additions and 125 deletions

View File

@ -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<Message> 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<Message> msg) throws SignatureException;
/**
* Clear the signing key (will require authentication to use again).
*/
public void clearSigningKey();
}

View File

@ -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.
* <p>
* <p/>
* 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<ByteString, PublicKey> loadedCertificates = new HashMap<>();
Map<ByteString, Certificate> 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<Message> 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<Message> 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<String> 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<Message> 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() {

View File

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

View File

@ -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) {

View File

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