Initial signature implementation with some tests

signature-implementation
Tal Moran 2015-11-12 03:05:19 +02:00
parent 0e69214f30
commit 5e80998d53
12 changed files with 403 additions and 6 deletions

View File

@ -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.+'

View File

@ -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<Message> 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<Message> 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<Message> msg) throws SignatureException;
public void clearSigningKey();
public boolean verify(Signature sig, List<Message> msgs);
}

View File

@ -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.
* <p>
* 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<ByteString, PublicKey> 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<? extends Certificate> 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<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());
}
}
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<Message> 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<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);
}
}
} 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<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;
}
}
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
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAECQ9ZFugbDZEWPbbJWroKTiTmqCl+Lgc+
EkPepT2j1pGrTVYEHIcDXgf4XY2cDbsBOwO1wMK+vRxZoV0YbDNNOQ==
-----END PUBLIC KEY-----

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEe9f5FRsgN4XcXCwQU9zT0Bekdy4aJZi0
TaoZ7x6syVrsu3r/l27sbbfQDdGo8nhQ8vDntW1EonZy8kIvp1wtHw==
-----END PUBLIC KEY-----