From 7e4260b8d5bad6017d2111a426f6e187b16d84e5 Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Sat, 24 Jun 2017 03:15:59 +0300 Subject: [PATCH] Added signature key generation to crypto API --- meerkat-common/build.gradle | 3 +- .../GenericBulletinBoardSignature.java | 8 + .../java/meerkat/crypto/DigitalSignature.java | 21 ++- .../crypto/DigitalSignatureGenerator.java | 38 ++++ .../crypto/concrete/ECDSASignature.java | 166 +++++++++++++++--- .../crypto/concrete/GlobalCryptoSetup.java | 10 +- .../src/main/proto/meerkat/crypto.proto | 1 + .../crypto/concrete/ECDSASignatureTest.java | 20 +++ 8 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 meerkat-common/src/main/java/meerkat/crypto/DigitalSignatureGenerator.java diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index 680f01b..2c29a9e 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -57,7 +57,8 @@ dependencies { // Crypto compile 'org.factcenter.qilin:qilin:1.2.+' - compile 'org.bouncycastle:bcprov-jdk15on:1.53' + compile 'org.bouncycastle:bcprov-jdk15on:1.57' + compile 'org.bouncycastle:bcpkix-jdk15on:1.57' // For certificate generation testCompile 'junit:junit:4.+' diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardSignature.java b/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardSignature.java index aad6466..b556bf9 100644 --- a/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardSignature.java +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardSignature.java @@ -48,6 +48,11 @@ public class GenericBulletinBoardSignature implements BulletinBoardSignature { } + @Override + public void loadVerificationCertificate(Crypto.SignatureVerificationKey cert) throws CertificateException { + signer.loadVerificationCertificate(cert); + } + @Override public void loadVerificationCertificates(InputStream certStream) throws CertificateException { signer.loadVerificationCertificates(certStream); @@ -98,6 +103,9 @@ public class GenericBulletinBoardSignature implements BulletinBoardSignature { return signer.getSignerID(); } + @Override + public Crypto.SignatureVerificationKey getSignerPublicKey() { return signer.getSignerPublicKey(); } + @Override public void clearSigningKey() { signer.clearSigningKey(); diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index 707e500..b6f0415 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -22,6 +22,18 @@ import static meerkat.protobuf.Crypto.*; public interface DigitalSignature { final public static String CERTIFICATE_ENCODING_X509 = "X.509"; + + /** + * Load a verification certificate. + * This will be added to the existing set of verification certificates. + * @param cert + * + * @throws CertificateException on parsing errors + */ + public void loadVerificationCertificate(SignatureVerificationKey cert) + throws CertificateException; + + /** * Load a set of certificates from an input stream. * This will consume the entire stream. @@ -94,6 +106,8 @@ public interface DigitalSignature { public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException; + + /** * Loads a private signing key. The keystore must include both the public and private * key parts. @@ -111,9 +125,14 @@ public interface DigitalSignature { */ public ByteString getSignerID(); + /** + * + * @return the signer verification key if it exists, null otherwise. + */ + public SignatureVerificationKey getSignerPublicKey(); + /** * Clear the signing key (will require authentication to use again). */ public void clearSigningKey(); - } diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignatureGenerator.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignatureGenerator.java new file mode 100644 index 0000000..f431440 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignatureGenerator.java @@ -0,0 +1,38 @@ +package meerkat.crypto; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.Date; + +/** + * A {@link DigitalSignature} that supports key generation. + */ +public interface DigitalSignatureGenerator extends DigitalSignature { + /** + * Generate a new verification key / signature key pair. + * The keypair is loaded as the default signing key. + * + * @param serial serial number of certificate + * @param notBefore earliest valid time + * @param notAfter expiration + * @param subjectName name corresponding to cert (will be used as "common name" in x509 cert). + */ + public void generateSigningCertificate(BigInteger serial, Date notBefore, Date notAfter, + String subjectName); + + + /** + * Store a keypair in a given keystore. + * + * @param store + * @param alias + * @param password + * @throws IOException + * @throws KeyStoreException + */ + public void storeSignatureKeypair(KeyStore store, String alias, char[] password) + throws IOException, KeyStoreException; +} diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java index e775226..eef377c 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -1,28 +1,39 @@ package meerkat.crypto.concrete; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.security.*; -import java.security.cert.*; -import java.security.cert.Certificate; -import java.util.*; - import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import meerkat.crypto.DigitalSignatureGenerator; import meerkat.protobuf.Crypto; +import meerkat.protobuf.Crypto.Signature; import meerkat.util.Hex; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.*; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.protobuf.Message; - -import meerkat.crypto.DigitalSignature; -import meerkat.protobuf.Crypto.Signature; - import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.*; +import java.security.spec.ECGenParameterSpec; +import java.util.*; /** @@ -30,7 +41,7 @@ import javax.security.auth.callback.UnsupportedCallbackException; * * This class is not thread-safe (each thread should have its own instance). */ -public class ECDSASignature implements DigitalSignature { +public class ECDSASignature implements DigitalSignatureGenerator { private static GlobalCryptoSetup globalCryptoSetup = GlobalCryptoSetup.getInstance(); @@ -38,6 +49,9 @@ public class ECDSASignature implements DigitalSignature { final public static String KEYSTORE_TYPE = "PKCS12"; final public static String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withECDSA"; + final public static String DEFAULT_KEY_ALGORITHM = "ECDSA"; + final public static String DEFAULT_KEY_CURVE = "secp256k1"; + SHA256Digest certDigest = new SHA256Digest(); @@ -56,6 +70,8 @@ public class ECDSASignature implements DigitalSignature { ByteString loadedSigningKeyId = null; + Certificate loadedSigningCert = null; + /** * The actual signing implementation. (used for both signing and verifying) */ @@ -100,19 +116,32 @@ public class ECDSASignature implements DigitalSignature { } } + + + public void loadVerificationCertificate(Certificate cert) throws CertificateException { + // Just checking + if (!(cert instanceof X509Certificate)) { + logger.error("Certificate must be in X509 format; got {} instead!", cert.getClass().getCanonicalName()); + return; + } + ByteString keyId = computeCertificateFingerprint(cert); + loadedCertificates.put(keyId, cert); + } + + + @Override + public void loadVerificationCertificate(Crypto.SignatureVerificationKey cert) throws CertificateException { + ByteArrayInputStream certStream = new ByteArrayInputStream(cert.getData().toByteArray()); + loadVerificationCertificates(certStream); + } + @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; - } - ByteString keyId = computeCertificateFingerprint(cert); - loadedCertificates.put(keyId, cert); + loadVerificationCertificate(cert); } } @@ -225,6 +254,15 @@ public class ECDSASignature implements DigitalSignature { } + + public void loadSigningCertificate(Certificate cert, PrivateKey key) throws InvalidKeyException, CertificateException { + loadedSigningKey = (PrivateKey) key; + loadedSigningKeyId = computeCertificateFingerprint(cert); + loadedSigningCert = cert; + signer.initSign(loadedSigningKey); + logger.debug("Loaded signing key with ID {}", Hex.encode(loadedSigningKeyId)); + } + /** * For now we only support PKCS12. * TODO: Support for PKCS11 as well. @@ -283,11 +321,7 @@ public class ECDSASignature implements DigitalSignature { } logger.trace("keystore entry {}, has key type {}", alias, key.getClass()); if (key instanceof PrivateKey) { - loadedSigningKey = (PrivateKey) key; - loadedSigningKeyId = computeCertificateFingerprint(cert); - signer.initSign(loadedSigningKey); - logger.debug("Loaded signing key with ID {}", Hex.encode(loadedSigningKeyId)); - + loadSigningCertificate(cert, (PrivateKey) key); return; } else { logger.info("Certificate {} in keystore does not have a private key", cert.toString()); @@ -314,6 +348,38 @@ public class ECDSASignature implements DigitalSignature { return loadedSigningKeyId; } + @Override + public Crypto.SignatureVerificationKey getSignerPublicKey() { + Crypto.SignatureType certType; + + if (loadedSigningCert instanceof X509Certificate) { + X509Certificate xcert = (X509Certificate) loadedSigningCert; + String algName = xcert.getSigAlgName(); + if (algName.endsWith("ECDSA")) { + certType = Crypto.SignatureType.ECDSA; + } else if (algName.endsWith("DSA")) { + certType = Crypto.SignatureType.DSA; + } else if (algName.endsWith("RSA")) { + certType = Crypto.SignatureType.RSA; + } else { + logger.error("Unknown certificate signature algorithm: {}", algName); + throw new RuntimeException("Unknown certificate signature algorithm: " + algName); + } + } else { + logger.error("We don't know how to handle non-X509 certificates"); + throw new RuntimeException("Unknown Certificate type: " + loadedSigningCert.getClass()); + } + try { + return Crypto.SignatureVerificationKey.newBuilder() + .setType(certType) + .setData(ByteString.copyFrom(loadedSigningCert.getEncoded())) + .build(); + } catch (CertificateEncodingException e) { + logger.error("CertificateEncodingException: ", e); + throw new RuntimeException(e); + } + } + public void clearSigningKey() { try { // TODO: Check if this really clears the key from memory @@ -321,6 +387,7 @@ public class ECDSASignature implements DigitalSignature { signer.initSign(null); loadedSigningKeyId = null; loadedSigningKey = null; + loadedSigningCert = null; // Start garbage collection? } catch (InvalidKeyException e) { // Do nothing @@ -328,4 +395,51 @@ public class ECDSASignature implements DigitalSignature { } + @Override + public void generateSigningCertificate(BigInteger serial, Date notBefore, Date notAfter, + String subjectName) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(DEFAULT_KEY_ALGORITHM); + ECGenParameterSpec kpgparams = new ECGenParameterSpec(DEFAULT_KEY_CURVE); + keyGen.initialize(kpgparams); + KeyPair pair = keyGen.generateKeyPair(); + + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(pair.getPublic().getEncoded()); + + // build a certificate generator + X500Name subject = new X500Name("cn=" + subjectName); + X509v3CertificateBuilder builder = new X509v3CertificateBuilder(subject, serial, notBefore, notAfter, + subject, subPubKeyInfo); + + // TODO: Might need to explicitly force SHA256 digest + JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils(); + + builder.addExtension(Extension.subjectKeyIdentifier, false, extensionUtils.createSubjectKeyIdentifier(pair.getPublic())); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.digitalSignature | KeyUsage.cRLSign); + builder.addExtension(Extension.keyUsage, false, usage); + ASN1EncodableVector purposes = new ASN1EncodableVector(); + purposes.add(KeyPurposeId.id_kp_serverAuth); + purposes.add(KeyPurposeId.id_kp_clientAuth); + purposes.add(KeyPurposeId.anyExtendedKeyUsage); + builder.addExtension(Extension.extendedKeyUsage, false, new DERSequence(purposes)); + + String signatureAlgorithm = DEFAULT_SIGNATURE_ALGORITHM; + + ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(pair.getPrivate()); + X509Certificate cert = new JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)); + + loadSigningCertificate(cert, pair.getPrivate()); + } catch (Exception e) { + // Convert all checked exceptions into runtime exceptions. + // TODO: Handle exceptions more elegantly. + throw new RuntimeException(e); + } + } + + @Override + public void storeSignatureKeypair(KeyStore store, String alias, char[] password) throws IOException, KeyStoreException { + Certificate chain[] = { loadedSigningCert }; + store.setKeyEntry(alias, loadedSigningKey, password, chain); + } } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java index 3d12701..4e7f4ce 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java @@ -46,10 +46,14 @@ public final class GlobalCryptoSetup { bouncyCastleProvider = new BouncyCastleProvider(); // Make bouncycastle our default provider if we're running on a JVM version < 8 // (earlier version don't support the EC curve we use for signatures) - if (!hasSecp256k1Curve() && !loadedBouncyCastle) { + if (!loadedBouncyCastle) { loadedBouncyCastle = true; - Security.insertProviderAt(bouncyCastleProvider, 1); - logger.info("Using BouncyCastle instead of native provider to support secp256k1 named curve"); + if (!hasSecp256k1Curve()) { + Security.insertProviderAt(bouncyCastleProvider, 1); + logger.info("Using BouncyCastle as default instead of native provider to support secp256k1 named curve"); + } else { + Security.addProvider(bouncyCastleProvider); + } } } } diff --git a/meerkat-common/src/main/proto/meerkat/crypto.proto b/meerkat-common/src/main/proto/meerkat/crypto.proto index eeec159..3cd5f80 100644 --- a/meerkat-common/src/main/proto/meerkat/crypto.proto +++ b/meerkat-common/src/main/proto/meerkat/crypto.proto @@ -7,6 +7,7 @@ option java_package = "meerkat.protobuf"; enum SignatureType { ECDSA = 0; DSA = 1; + RSA = 2; } message BigInteger { diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java index e5b448d..c02d880 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java @@ -9,6 +9,7 @@ import org.junit.Test; import java.io.InputStream; import java.math.BigInteger; import java.security.KeyStore; +import java.util.Date; import java.util.Random; import static org.junit.Assert.assertFalse; @@ -215,5 +216,24 @@ public class ECDSASignatureTest { } + @Test + public void generateKeysSignAndVerify() throws Exception { + signer.generateSigningCertificate(BigInteger.ONE, new Date(), new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 365), "testing"); + + Crypto.SignatureVerificationKey verificationKey = signer.getSignerPublicKey(); + + signer.loadVerificationCertificate(verificationKey); + + BigInteger rawMsg = new BigInteger(50, rand); + Crypto.BigInteger usMsg = Crypto.BigInteger.newBuilder() + .setData(ByteString.copyFrom(rawMsg.toByteArray())).build(); + + signer.updateContent(usMsg); + Crypto.Signature sig = signer.sign(); + + signer.initVerify(sig); + signer.updateContent(usMsg); + assertTrue("Couldn't verify signature on ", signer.verify()); + } }