Added signature key generation to crypto API

android-scanner
Tal Moran 2017-06-24 03:15:59 +03:00
parent ceba09e65c
commit 7e4260b8d5
8 changed files with 236 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ option java_package = "meerkat.protobuf";
enum SignatureType {
ECDSA = 0;
DSA = 1;
RSA = 2;
}
message BigInteger {

View File

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