Initial signature implementation with some tests
parent
0e69214f30
commit
5e80998d53
|
@ -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.+'
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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