copy org.fdroid.fdroid.localrepo.LocalRepoKeystore from Kerplapp
This is the keystore used for both the index.jar signing key and the key and certificate needed to support HTTPS connections. https://github.com/guardianproject/Kerplapp refs #3380 https://dev.guardianproject.info/issues/3380
This commit is contained in:
parent
b0c27869b6
commit
544b924ab9
357
src/org/fdroid/fdroid/localrepo/LocalRepoKeyStore.java
Normal file
357
src/org/fdroid/fdroid/localrepo/LocalRepoKeyStore.java
Normal file
@ -0,0 +1,357 @@
|
||||
|
||||
package org.fdroid.fdroid.localrepo;
|
||||
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.spongycastle.asn1.ASN1Sequence;
|
||||
import org.spongycastle.asn1.x500.X500Name;
|
||||
import org.spongycastle.asn1.x509.*;
|
||||
import org.spongycastle.cert.X509CertificateHolder;
|
||||
import org.spongycastle.cert.X509v3CertificateBuilder;
|
||||
import org.spongycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.spongycastle.operator.ContentSigner;
|
||||
import org.spongycastle.operator.OperatorCreationException;
|
||||
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.net.Socket;
|
||||
import java.security.*;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Formatter;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.X509KeyManager;
|
||||
|
||||
import kellinwood.security.zipsigner.ZipSigner;
|
||||
|
||||
public class LocalRepoKeyStore {
|
||||
// TODO: Address exception handling in a uniform way across the KeyStore & application
|
||||
|
||||
static {
|
||||
Security.insertProviderAt(
|
||||
new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
|
||||
}
|
||||
|
||||
private static final String TAG = "KerplappKeyStore";
|
||||
|
||||
public static final String INDEX_CERT_ALIAS = "fdroid";
|
||||
public static final String HTTP_CERT_ALIAS = "https";
|
||||
|
||||
private static final String DEFAULT_SIG_ALG = "SHA1withRSA";
|
||||
private static final String DEFAULT_KEY_ALGO = "RSA";
|
||||
private static final int DEFAULT_KEY_BITS = 2048;
|
||||
|
||||
private static final String DEFAULT_INDEX_CERT_INFO = "O=Kerplapp,OU=GuardianProject";
|
||||
|
||||
private KeyStore keyStore;
|
||||
private KeyManager[] keyManagers;
|
||||
private File backingFile;
|
||||
|
||||
public LocalRepoKeyStore(File backingFile) throws KeyStoreException, NoSuchAlgorithmException,
|
||||
CertificateException, IOException, OperatorCreationException, UnrecoverableKeyException {
|
||||
this.backingFile = backingFile;
|
||||
this.keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
|
||||
// If there isn't a persisted BKS keystore on disk we need to
|
||||
// create a new empty keystore
|
||||
if (!backingFile.exists())
|
||||
{
|
||||
// Init a new keystore with a blank passphrase
|
||||
keyStore.load(null, "".toCharArray());
|
||||
} else {
|
||||
keyStore.load(new FileInputStream(backingFile), "".toCharArray());
|
||||
}
|
||||
|
||||
/*
|
||||
* If the keystore we loaded doesn't have an INDEX_CERT_ALIAS entry we
|
||||
* need to generate a new random keypair and a self signed certificate
|
||||
* for this slot.
|
||||
*/
|
||||
if (keyStore.getKey(INDEX_CERT_ALIAS, "".toCharArray()) == null)
|
||||
{
|
||||
// Generate a random key pair to associate with the INDEX_CERT_ALIAS
|
||||
// certificate in the keystore. This keypair will be used for the
|
||||
// HTTPS cert as well.
|
||||
KeyPair rndKeys = generateRandomKeypair();
|
||||
|
||||
// Generate a self signed certificate for signing the index.jar
|
||||
// We can't generate the HTTPS certificate until we know what the IP
|
||||
// address will be to use for the CN field.
|
||||
X500Name subject = new X500Name(DEFAULT_INDEX_CERT_INFO);
|
||||
Certificate indexCert = generateSelfSignedCertChain(rndKeys, subject);
|
||||
|
||||
addToStore(INDEX_CERT_ALIAS, rndKeys, indexCert);
|
||||
}
|
||||
|
||||
// Kerplapp uses its own KeyManager to to ensure the correct keystore
|
||||
// alias is used for the correct purpose. With the default key manager
|
||||
// it is not possible to specify that HTTP_CERT_ALIAS should be used for
|
||||
// TLS and INDEX_CERT_ALIAS for signing the index.jar.
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory
|
||||
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
|
||||
keyManagerFactory.init(keyStore, "".toCharArray());
|
||||
KeyManager defaultKeyManager = keyManagerFactory.getKeyManagers()[0];
|
||||
KeyManager wrappedKeyManager = new KerplappKeyManager(
|
||||
(X509KeyManager) defaultKeyManager);
|
||||
keyManagers = new KeyManager[] {
|
||||
wrappedKeyManager
|
||||
};
|
||||
}
|
||||
|
||||
public void setupHTTPSCertificate() throws CertificateException,
|
||||
OperatorCreationException, KeyStoreException, NoSuchAlgorithmException,
|
||||
FileNotFoundException, IOException, UnrecoverableKeyException {
|
||||
// Get the existing private/public keypair to use for the HTTPS cert
|
||||
KeyPair kerplappKeypair = getKerplappKeypair();
|
||||
|
||||
/*
|
||||
* Once we have an IP address, that can be used as the hostname. We can
|
||||
* generate a self signed cert with a valid CN field to stash into the
|
||||
* keystore in a predictable place. If the IP address changes we should
|
||||
* run this method again to stomp old HTTPS_CERT_ALIAS entries.
|
||||
*/
|
||||
X500Name subject = new X500Name("CN=" + FDroidApp.ipAddressString);
|
||||
|
||||
Certificate indexCert = generateSelfSignedCertChain(kerplappKeypair, subject,
|
||||
FDroidApp.ipAddressString);
|
||||
|
||||
addToStore(HTTP_CERT_ALIAS, kerplappKeypair, indexCert);
|
||||
}
|
||||
|
||||
public File getKeyStoreFile() {
|
||||
return backingFile;
|
||||
}
|
||||
|
||||
public KeyStore getKeyStore() {
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
public KeyManager[] getKeyManagers() {
|
||||
return keyManagers;
|
||||
}
|
||||
|
||||
public void signZip(File input, File output) {
|
||||
try {
|
||||
ZipSigner zipSigner = new ZipSigner();
|
||||
|
||||
KeyStore keystore = getKeyStore();
|
||||
X509Certificate cert = (X509Certificate) keystore.getCertificate(INDEX_CERT_ALIAS);
|
||||
|
||||
KeyPair kp = getKerplappKeypair();
|
||||
PrivateKey priv = kp.getPrivate();
|
||||
|
||||
zipSigner.setKeys("kerplapp", cert, priv, DEFAULT_SIG_ALG, null);
|
||||
zipSigner.signZip(input.getAbsolutePath(), output.getAbsolutePath());
|
||||
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InstantiationException e) {
|
||||
e.printStackTrace();
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyPair getKerplappKeypair() throws KeyStoreException, UnrecoverableKeyException,
|
||||
NoSuchAlgorithmException {
|
||||
/*
|
||||
* You can't store a keypair without an associated certificate chain so,
|
||||
* we'll use the INDEX_CERT_ALIAS as the de-facto keypair/certificate
|
||||
* chain. This cert/key is initialized when the KerplappKeyStore is
|
||||
* constructed for the first time and should *always* be present.
|
||||
*/
|
||||
Key key = keyStore.getKey(INDEX_CERT_ALIAS, "".toCharArray());
|
||||
|
||||
if (key instanceof PrivateKey) {
|
||||
Certificate cert = keyStore.getCertificate(INDEX_CERT_ALIAS);
|
||||
PublicKey publicKey = cert.getPublicKey();
|
||||
return new KeyPair(publicKey, (PrivateKey) key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is take from FDroid: org.fdroid.fdroid.DB.calcFingerprint()
|
||||
// TODO once this code is part of FDroid, replace this with DB.calcFingerprint()
|
||||
public String getFingerprint() {
|
||||
String ret = null;
|
||||
try {
|
||||
Certificate cert = getCertificate();
|
||||
if (cert != null) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(cert.getEncoded());
|
||||
byte[] fingerprint = digest.digest();
|
||||
Formatter formatter = new Formatter(new StringBuilder());
|
||||
for (int i = 0; i < fingerprint.length; i++) {
|
||||
formatter.format("%02X", fingerprint[i]);
|
||||
}
|
||||
ret = formatter.toString();
|
||||
formatter.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public Certificate getCertificate() {
|
||||
try {
|
||||
Key key = keyStore.getKey(INDEX_CERT_ALIAS, "".toCharArray());
|
||||
if (key instanceof PrivateKey)
|
||||
return keyStore.getCertificate(INDEX_CERT_ALIAS);
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addToStore(String alias, KeyPair kp, Certificate cert) throws KeyStoreException,
|
||||
NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException,
|
||||
UnrecoverableKeyException {
|
||||
Certificate[] chain = new Certificate[] {
|
||||
cert
|
||||
};
|
||||
keyStore.setKeyEntry(alias, kp.getPrivate(),
|
||||
"".toCharArray(), chain);
|
||||
|
||||
keyStore.store(new FileOutputStream(backingFile), "".toCharArray());
|
||||
|
||||
/*
|
||||
* After adding an entry to the keystore we need to create a fresh
|
||||
* KeyManager by reinitializing the KeyManagerFactory with the new key
|
||||
* store content and then rewrapping the default KeyManager with our own
|
||||
*/
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory
|
||||
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
|
||||
keyManagerFactory.init(keyStore, "".toCharArray());
|
||||
KeyManager defaultKeyManager = keyManagerFactory.getKeyManagers()[0];
|
||||
KeyManager wrappedKeyManager = new KerplappKeyManager((X509KeyManager) defaultKeyManager);
|
||||
keyManagers = new KeyManager[] {
|
||||
wrappedKeyManager
|
||||
};
|
||||
}
|
||||
|
||||
private KeyPair generateRandomKeypair() throws NoSuchAlgorithmException {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(DEFAULT_KEY_ALGO);
|
||||
keyPairGenerator.initialize(DEFAULT_KEY_BITS);
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
private Certificate generateSelfSignedCertChain(KeyPair kp, X500Name subject)
|
||||
throws CertificateException, OperatorCreationException, IOException {
|
||||
return generateSelfSignedCertChain(kp, subject, null);
|
||||
}
|
||||
|
||||
private Certificate generateSelfSignedCertChain(KeyPair kp, X500Name subject, String hostname)
|
||||
throws CertificateException, OperatorCreationException, IOException {
|
||||
SecureRandom rand = new SecureRandom();
|
||||
PrivateKey privKey = kp.getPrivate();
|
||||
PublicKey pubKey = kp.getPublic();
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIG_ALG).build(privKey);
|
||||
|
||||
SubjectPublicKeyInfo subPubKeyInfo = new SubjectPublicKeyInfo(
|
||||
ASN1Sequence.getInstance(pubKey.getEncoded()));
|
||||
|
||||
Date startDate = new Date(); // now
|
||||
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.setTime(startDate);
|
||||
c.add(Calendar.YEAR, 1);
|
||||
Date endDate = c.getTime();
|
||||
|
||||
X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(
|
||||
subject,
|
||||
BigInteger.valueOf(rand.nextLong()),
|
||||
startDate, endDate,
|
||||
subject,
|
||||
subPubKeyInfo);
|
||||
|
||||
if (hostname != null)
|
||||
{
|
||||
|
||||
GeneralNames subjectAltName = new GeneralNames(
|
||||
new GeneralName(GeneralName.iPAddress, hostname));
|
||||
|
||||
// X509Extension extension = new X509Extension(false, new
|
||||
// DEROctetString(subjectAltName));
|
||||
|
||||
v3CertGen.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
|
||||
}
|
||||
|
||||
X509CertificateHolder certHolder = v3CertGen.build(sigGen);
|
||||
return new JcaX509CertificateConverter().getCertificate(certHolder);
|
||||
}
|
||||
|
||||
/*
|
||||
* A X509KeyManager that always returns the KerplappKeyStore.HTTP_CERT_ALIAS
|
||||
* for it's chosen server alias. All other operations are deferred to the
|
||||
* wrapped X509KeyManager.
|
||||
*/
|
||||
private static class KerplappKeyManager implements X509KeyManager {
|
||||
private final X509KeyManager wrapped;
|
||||
|
||||
private KerplappKeyManager(X509KeyManager wrapped)
|
||||
{
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseClientAlias(String[] keyType, Principal[] issuers,
|
||||
Socket socket) {
|
||||
return wrapped.chooseClientAlias(keyType, issuers, socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||
Socket socket) {
|
||||
/*
|
||||
* Always use the HTTP_CERT_ALIAS for the server alias.
|
||||
*/
|
||||
return LocalRepoKeyStore.HTTP_CERT_ALIAS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
return wrapped.getCertificateChain(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||
return wrapped.getClientAliases(keyType, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
return wrapped.getPrivateKey(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||
return wrapped.getServerAliases(keyType, issuers);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user