Initial signature implementation with some tests
parent
0e69214f30
commit
5e80998d53
|
@ -45,6 +45,10 @@ dependencies {
|
||||||
// Google protobufs
|
// Google protobufs
|
||||||
compile 'com.google.protobuf:protobuf-java:3.+'
|
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.+'
|
testCompile 'junit:junit:4.+'
|
||||||
|
|
||||||
runtime 'org.codehaus.groovy:groovy:2.4.+'
|
runtime 'org.codehaus.groovy:groovy:2.4.+'
|
||||||
|
|
|
@ -2,16 +2,69 @@ package meerkat.crypto;
|
||||||
|
|
||||||
import com.google.protobuf.Message;
|
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 java.util.List;
|
||||||
import static meerkat.protobuf.Crypto.*;
|
import static meerkat.protobuf.Crypto.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by talm on 25/10/15.
|
* Created by talm on 25/10/15.
|
||||||
*
|
*
|
||||||
* Sign arrays of messages
|
* Sign and verifyarrays of messages
|
||||||
*/
|
*/
|
||||||
public interface DigitalSignature { // Extends SCAPI DigitalSignature
|
public interface DigitalSignature {
|
||||||
public Signature sign(List<Message> msg);
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package meerkat.crypto.concrete;
|
package meerkat.crypto.concrete;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.Message;
|
import com.google.protobuf.Message;
|
||||||
import meerkat.crypto.Digest;
|
import meerkat.crypto.Digest;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -32,7 +33,7 @@ public class SHA256Digest implements Digest {
|
||||||
public SHA256Digest() { this(true); }
|
public SHA256Digest() { this(true); }
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**SHA
|
||||||
* Instantiate with the default (SHA-256) algorithm,
|
* Instantiate with the default (SHA-256) algorithm,
|
||||||
* or create an empty class (for cloning)
|
* or create an empty class (for cloning)
|
||||||
*/
|
*/
|
||||||
|
@ -58,6 +59,14 @@ public class SHA256Digest implements Digest {
|
||||||
hash.update(msg.toByteString().asReadOnlyByteBuffer());
|
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
|
@Override
|
||||||
public void reset() {
|
public void reset() {
|
||||||
hash.reset();
|
hash.reset();
|
||||||
|
|
|
@ -13,14 +13,18 @@ enum SignatureType {
|
||||||
message Signature {
|
message Signature {
|
||||||
SignatureType type = 1;
|
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;
|
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
|
// Public key used to verify signatures
|
||||||
message SignatureVerificationKey {
|
message SignatureVerificationKey {
|
||||||
SignatureType type = 1;
|
SignatureType type = 1;
|
||||||
|
|
||||||
|
// Data encoding depends on type; default is x509 DER-encoded
|
||||||
bytes data = 2;
|
bytes data = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,4 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAECQ9ZFugbDZEWPbbJWroKTiTmqCl+Lgc+
|
||||||
|
EkPepT2j1pGrTVYEHIcDXgf4XY2cDbsBOwO1wMK+vRxZoV0YbDNNOQ==
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -0,0 +1,4 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEe9f5FRsgN4XcXCwQU9zT0Bekdy4aJZi0
|
||||||
|
TaoZ7x6syVrsu3r/l27sbbfQDdGo8nhQ8vDntW1EonZy8kIvp1wtHw==
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -0,0 +1 @@
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue