diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index 9768dd8..b744fcd 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -45,6 +45,10 @@ dependencies { // Google protobufs compile 'com.google.protobuf:protobuf-java:3.+' + // Crypto + compile 'org.bouncycastle:bcprov-jdk15on:1.53' + compile 'org.bouncycastle:bcpkix-jdk15on:1.53' + testCompile 'junit:junit:4.+' runtime 'org.codehaus.groovy:groovy:2.4.+' diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index 691b492..cc9151e 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -2,16 +2,69 @@ package meerkat.crypto; import com.google.protobuf.Message; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.List; import static meerkat.protobuf.Crypto.*; /** * Created by talm on 25/10/15. * - * Sign arrays of messages + * Sign and verifyarrays of messages */ -public interface DigitalSignature { // Extends SCAPI DigitalSignature - public Signature sign(List msg); +public interface DigitalSignature { + final public static String CERTIFICATE_ENCODING_X509 = "X.509"; + + /** + * Load a set of certificates from an input stream. + * 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)}. + * @param certStream source from which certificates are loaded + * @throws CertificateException on parsing errors + */ + public void loadVerificationCertificates(InputStream certStream) + throws CertificateException; + + /** + * Clear the loaded verification certificates. + */ + 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 + * + * @param sig The signature on the messages + * @param msgs The messages themselves + * @return + * @throws CertificateException the certificate required for verification was not loaded. + */ + public boolean verify(Signature sig, List msgs) + throws CertificateException, InvalidKeyException; + + /** + * Loads a private signing key. The certificate must be in PKCS12 format and include both + * the public and private keys. + * This method must be called before calling {@link #sign(List)} + * Calling this method again will replace the key. + * + * @param keyStream + */ + public void loadSigningCertificate(InputStream keyStream, char[] password) + throws IOException, CertificateException, UnrecoverableKeyException; + + public Signature sign(List msg) throws SignatureException; + + public void clearSigningKey(); - public boolean verify(Signature sig, List msgs); } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java new file mode 100644 index 0000000..a947e15 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -0,0 +1,239 @@ +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.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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.protobuf.Message; + +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.Crypto.Signature; +import org.bouncycastle.util.io.pem.*; +import org.bouncycastle.openssl.*; + + +/** + * Sign and verify digital signatures. + *

+ * This class is not thread-safe (each thread should have its own instance). + */ +public class ECDSASignature implements DigitalSignature { + final Logger logger = LoggerFactory.getLogger(getClass()); + + final public static String KEYSTORE_TYPE = "PKCS12"; + final public static String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withECDSA"; + + SHA256Digest digest = new SHA256Digest(); + + Map loadedCertificates = new HashMap<>(); + + ByteString loadedSigningKeyId = null; + + /** + * The actual signing implementation. + */ + java.security.Signature signer; + + + /** + * Compute a fingerprint of a public key as a SHA256 hash. + * + * @param pubKey + * @return + */ + public ByteString computePublicKeyID(PublicKey pubKey) { + digest.reset(); + byte[] data = pubKey.getEncoded(); + digest.update(data); + return ByteString.copyFrom(digest.digest()); + } + + public ECDSASignature(java.security.Signature signer) { + this.signer = signer; + } + + public ECDSASignature() { + try { + 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); + } + } + + @Override + public void loadVerificationCertificates(InputStream certStream) + throws CertificateException { + CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_ENCODING_X509); + Collection certs = certificateFactory.generateCertificates(certStream); + for (Certificate cert : certs) { + // Just checking + if (!(cert instanceof X509Certificate)) { + logger.error("Certificate must be in X509 format; got {} instead!", cert.getClass().getCanonicalName()); + continue; + } + PublicKey pubKey = cert.getPublicKey(); + ByteString keyId = computePublicKeyID(pubKey); + loadedCertificates.put(keyId, pubKey); + } + } + + @Override + public void clearVerificationCertificates() { + loadedCertificates.clear(); + } + + + 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()); + } + } + + public void updateSigner(InputStream in) throws IOException, SignatureException { + ByteString inStr = ByteString.readFrom(in); + signer.update(inStr.asReadOnlyByteBuffer()); + } + + public Signature sign() throws SignatureException { + Signature.Builder sig = Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + sig.setData(ByteString.copyFrom(signer.sign())); + sig.setSignerId(loadedSigningKeyId); + return sig.build(); + } + + 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; + } + } + + @Override + public boolean verify(Signature sig, List msgs) + throws CertificateException { + try { + initVerify(sig); + updateSigner(msgs); + return verify(sig); + } catch (InvalidKeyException e) { + logger.error("Invalid key exception: {}", e); + return false; + } catch (SignatureException e) { + // Should never happen! + logger.error("Signature exception: {}", e); + return false; + } + } + + @Override + public void loadSigningCertificate(InputStream keyStream, char[] password) + throws IOException, CertificateException, UnrecoverableKeyException { + try { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + + keyStore.load(keyStream, password); + + KeyStore.ProtectionParameter protParam = + new KeyStore.PasswordProtection(password); + + // 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); + } + + } + } catch (KeyStoreException e) { + logger.error("Keystore exception: {}", e); + } catch (NoSuchAlgorithmException 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; + } + } + + public void clearSigningKey() { + try { + // TODO: Check if this really clears the key from memory + if (loadedSigningKeyId != null) + signer.initSign(null); + loadedSigningKeyId = null; + } 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 092aab4..8e72f26 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java @@ -1,5 +1,6 @@ package meerkat.crypto.concrete; +import com.google.protobuf.ByteString; import com.google.protobuf.Message; import meerkat.crypto.Digest; import org.slf4j.Logger; @@ -32,7 +33,7 @@ public class SHA256Digest implements Digest { public SHA256Digest() { this(true); } - /** + /**SHA * Instantiate with the default (SHA-256) algorithm, * or create an empty class (for cloning) */ @@ -58,6 +59,14 @@ public class SHA256Digest implements Digest { hash.update(msg.toByteString().asReadOnlyByteBuffer()); } + final public void update(ByteString msg) { + hash.update(msg.asReadOnlyByteBuffer()); + } + + final public void update(byte[] msg) { + hash.update(msg); + } + @Override public void reset() { hash.reset(); diff --git a/meerkat-common/src/main/proto/meerkat/crypto.proto b/meerkat-common/src/main/proto/meerkat/crypto.proto index e1475da..feafc1c 100644 --- a/meerkat-common/src/main/proto/meerkat/crypto.proto +++ b/meerkat-common/src/main/proto/meerkat/crypto.proto @@ -13,14 +13,18 @@ enum SignatureType { message Signature { SignatureType type = 1; - // Data encoding depends on type; default is x509 BER-encoded + // Data encoding depends on type; default is DER-encoded bytes data = 2; + + // ID of the signer (should be the fingerprint of the signature verification key) + bytes signer_id = 3; } // Public key used to verify signatures message SignatureVerificationKey { SignatureType type = 1; + // Data encoding depends on type; default is x509 DER-encoded bytes data = 2; } diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java new file mode 100644 index 0000000..99dbee1 --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java @@ -0,0 +1,79 @@ +package meerkat.crypto.concrete; + +import com.google.protobuf.ByteString; +import meerkat.protobuf.Crypto; +import org.junit.Test; + +import java.io.InputStream; + +import static org.junit.Assert.assertTrue; + +/** + * Created by talm on 12/11/15. + */ +public class TestECDSASignature { + public static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; + public static String KEYFILE_PASSWORD = "secret"; + + public static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; + public static String CERT2_DER_EXAMPLE = "/certs/enduser-certs/user2.der"; + + public static String MSG_PLAINTEXT_EXAMPLE = "/certs/signed-messages/helloworld.txt"; + public static String MSG_SIG_EXAMPLE = "/certs/signed-messages/helloworld.txt.sha256sig"; + + + @Test + public void testLoadSignatureKey() throws Exception { + InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); + char[] password = KEYFILE_PASSWORD.toCharArray(); + + ECDSASignature sig = new ECDSASignature(); + + sig.loadSigningCertificate(keyStream, password); + keyStream.close(); + } + + @Test + public void loadPEMVerificationKey() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); + + ECDSASignature sig = new ECDSASignature(); + + sig.loadVerificationCertificates(certStream); + certStream.close(); + } + + @Test + public void loadDERVerificationKey() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT2_DER_EXAMPLE); + + ECDSASignature sig = new ECDSASignature(); + + sig.loadVerificationCertificates(certStream); + certStream.close(); + } + + + @Test + public void verifyValidSig() 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)); + + Crypto.Signature builtSig = sig.build(); + signer.initVerify(builtSig); + signer.updateSigner(msgStream); + assertTrue("Signature did not verify!", signer.verify(builtSig)); + } + +} diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.p12 b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.p12 new file mode 100644 index 0000000..6281f9d Binary files /dev/null and b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.p12 differ diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.der b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.der new file mode 100644 index 0000000..6f17406 Binary files /dev/null and b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.der differ diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-pubkey.pem b/meerkat-common/src/test/resources/certs/enduser-certs/user1-pubkey.pem new file mode 100644 index 0000000..1c0a0c1 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user1-pubkey.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAECQ9ZFugbDZEWPbbJWroKTiTmqCl+Lgc+ +EkPepT2j1pGrTVYEHIcDXgf4XY2cDbsBOwO1wMK+vRxZoV0YbDNNOQ== +-----END PUBLIC KEY----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user2-pubkey.pem b/meerkat-common/src/test/resources/certs/enduser-certs/user2-pubkey.pem new file mode 100644 index 0000000..5d86d4c --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user2-pubkey.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEe9f5FRsgN4XcXCwQU9zT0Bekdy4aJZi0 +TaoZ7x6syVrsu3r/l27sbbfQDdGo8nhQ8vDntW1EonZy8kIvp1wtHw== +-----END PUBLIC KEY----- diff --git a/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt b/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt @@ -0,0 +1 @@ + diff --git a/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt.sha256sig b/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt.sha256sig new file mode 100644 index 0000000..7e717d1 Binary files /dev/null and b/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt.sha256sig differ