android - 如何在 Android 中以编程方式生成自签名证书并将其用于 https 服务器

标签 android bouncycastle

我需要在 Android 应用程序中即时创建 SSL 自签名证书,并能够在同一应用程序中从 https 服务器使用它。我发现这段代码可以创建一个证书,尽管我不确定它是否是正确的证书。我还没有找到太多关于如何将它添加到我的应用程序上的 BouncyCaSTLe keystore 以及如何在创建 HTTPs 服务器时使用它的信息。有人可以指出我这样做的例子吗?谢谢你。

static X509Certificate generateSelfSignedX509Certificate() throws Exception {

        // yesterday
        Date validityBeginDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
        // in 2 years
        Date validityEndDate = new Date(System.currentTimeMillis() + 2 * 365 * 24 * 60 * 60 * 1000);

        // GENERATE THE PUBLIC/PRIVATE RSA KEY PAIR
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
        keyPairGenerator.initialize(1024, new SecureRandom());

        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        // GENERATE THE X509 CERTIFICATE
        X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
        X500Principal dnName = new X500Principal("CN=John Doe");

        certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
        certGen.setSubjectDN(dnName);
        certGen.setIssuerDN(dnName); // use the same
        certGen.setNotBefore(validityBeginDate);
        certGen.setNotAfter(validityEndDate);
        certGen.setPublicKey(keyPair.getPublic());
        certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");

        X509Certificate cert = certGen.generate(keyPair.getPrivate(), "BC");

        // DUMP CERTIFICATE AND KEY PAIR

        return cert;
        //  System.out.println(cert);


    }

最佳答案

以下解决方案适用于在 Android 上使用 Spongy CaSTLe (Bouncy CaSTLe) 生成自签名证书。我已经使用 Android 10 (Q) 和 Android Pie 测试了代码。

此代码是 Netty 的 io.netty.handler.ssl.util.SelfSignedCertificate 的修改版本。原始版本需要充气城堡;这似乎在 Android 10 上默认不存在,导致 java.lang.NoClassDefFoundError: org.spongycaSTLe.jce.provider.BouncyCaSTLeProvider。因此,我不得不复制代码并修改它以使其与 Spongy CaSTLe 一起工作。

build.gradle

dependencies {
    implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0'
}

SelfSignedCertificate.java

import android.util.Base64;
import android.util.Log;

import org.spongycastle.asn1.x500.X500Name;
import org.spongycastle.cert.X509CertificateHolder;
import org.spongycastle.cert.X509v3CertificateBuilder;
import org.spongycastle.cert.jcajce.JcaX509CertificateConverter;
import org.spongycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.operator.ContentSigner;
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;

public final class SelfSignedCertificate {

    private static final String TAG = SelfSignedCertificate.class.getSimpleName();

    /**
     * Current time minus 1 year, just in case software clock goes back due to time synchronization
     */
    private static final Date DEFAULT_NOT_BEFORE = new Date(System.currentTimeMillis() - 86400000L * 365);

    /**
     * The maximum possible value in X.509 specification: 9999-12-31 23:59:59
     */
    private static final Date DEFAULT_NOT_AFTER = new Date(253402300799000L);

    /**
     * FIPS 140-2 encryption requires the key length to be 2048 bits or greater.
     * Let's use that as a sane default but allow the default to be set dynamically
     * for those that need more stringent security requirements.
     */
    private static final int DEFAULT_KEY_LENGTH_BITS = 2048;

    /**
     * FQDN to use if none is specified.
     */
    private static final String DEFAULT_FQDN = "example.com";

    /**
     * 7-bit ASCII, as known as ISO646-US or the Basic Latin block of the
     * Unicode character set
     */
    private static final Charset US_ASCII = Charset.forName("US-ASCII");

    private static final Provider provider = new BouncyCastleProvider();

    private final File certificate;
    private final File privateKey;
    private final X509Certificate cert;
    private final PrivateKey key;

    /**
     * Creates a new instance.
     */
    public SelfSignedCertificate() throws CertificateException {
        this(DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
    }

    /**
     * Creates a new instance.
     *
     * @param notBefore Certificate is not valid before this time
     * @param notAfter  Certificate is not valid after this time
     */
    public SelfSignedCertificate(Date notBefore, Date notAfter) throws CertificateException {
        this("example.com", notBefore, notAfter);
    }

    /**
     * Creates a new instance.
     *
     * @param fqdn a fully qualified domain name
     */
    public SelfSignedCertificate(String fqdn) throws CertificateException {
        this(fqdn, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
    }

    /**
     * Creates a new instance.
     *
     * @param fqdn      a fully qualified domain name
     * @param notBefore Certificate is not valid before this time
     * @param notAfter  Certificate is not valid after this time
     */
    public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
        // Bypass entropy collection by using insecure random generator.
        // We just want to generate it without any delay because it's for testing purposes only.
        this(fqdn, new SecureRandom(), DEFAULT_KEY_LENGTH_BITS, notBefore, notAfter);
    }

    /**
     * Creates a new instance.
     *
     * @param fqdn   a fully qualified domain name
     * @param random the {@link java.security.SecureRandom} to use
     * @param bits   the number of bits of the generated private key
     */
    public SelfSignedCertificate(String fqdn, SecureRandom random, int bits) throws CertificateException {
        this(fqdn, random, bits, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
    }

    /**
     * Creates a new instance.
     *
     * @param fqdn      a fully qualified domain name
     * @param random    the {@link java.security.SecureRandom} to use
     * @param bits      the number of bits of the generated private key
     * @param notBefore Certificate is not valid before this time
     * @param notAfter  Certificate is not valid after this time
     */
    public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter)
        throws CertificateException {
        // Generate an RSA key pair.
        final KeyPair keypair;
        try {
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
            keyGen.initialize(bits, random);
            keypair = keyGen.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            // Should not reach here because every Java implementation must have RSA key pair generator.
            throw new Error(e);
        }

        String[] paths;
        try {
            // Try Bouncy Castle if the current JVM didn't have sun.security.x509.
            paths = generateCertificate(fqdn, keypair, random, notBefore, notAfter);
        } catch (Throwable t2) {
            Log.d(TAG, "Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
            throw new CertificateException("No provider succeeded to generate a self-signed certificate. See debug log for the root cause.", t2);
        }

        certificate = new File(paths[0]);
        privateKey = new File(paths[1]);
        key = keypair.getPrivate();
        FileInputStream certificateInput = null;
        try {
            certificateInput = new FileInputStream(certificate);
            cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
        } catch (Exception e) {
            throw new CertificateEncodingException(e);
        } finally {
            if (certificateInput != null) {
                try {
                    certificateInput.close();
                } catch (IOException e) {
                    Log.w(TAG, "Failed to close a file: " + certificate, e);
                }
            }
        }
    }

    /**
     * Returns the generated X.509 certificate file in PEM format.
     */
    public File certificate() {
        return certificate;
    }

    /**
     * Returns the generated RSA private key file in PEM format.
     */
    public File privateKey() {
        return privateKey;
    }

    /**
     * Returns the generated X.509 certificate.
     */
    public X509Certificate cert() {
        return cert;
    }

    /**
     * Returns the generated RSA private key.
     */
    public PrivateKey key() {
        return key;
    }

    /**
     * Deletes the generated X.509 certificate file and RSA private key file.
     */
    public void delete() {
        safeDelete(certificate);
        safeDelete(privateKey);
    }

    private static String[] generateCertificate(String fqdn, KeyPair keypair, SecureRandom random, Date notBefore, Date notAfter)
        throws Exception {
        PrivateKey key = keypair.getPrivate();

        // Prepare the information required for generating an X.509 certificate.
        X500Name owner = new X500Name("CN=" + fqdn);
        X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
            owner, new BigInteger(64, random), notBefore, notAfter, owner, keypair.getPublic());

        ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(key);
        X509CertificateHolder certHolder = builder.build(signer);
        X509Certificate cert = new JcaX509CertificateConverter().setProvider(provider).getCertificate(certHolder);
        cert.verify(keypair.getPublic());

        return newSelfSignedCertificate(fqdn, key, cert);
    }

    private static String[] newSelfSignedCertificate(String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
        String keyText = "-----BEGIN PRIVATE KEY-----\n" + Base64.encodeToString(key.getEncoded(), Base64.DEFAULT) + "\n-----END PRIVATE KEY-----\n";
        File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
        keyFile.deleteOnExit();

        OutputStream keyOut = new FileOutputStream(keyFile);
        try {
            keyOut.write(keyText.getBytes(US_ASCII));
            keyOut.close();
            keyOut = null;
        } finally {
            if (keyOut != null) {
                safeClose(keyFile, keyOut);
                safeDelete(keyFile);
            }
        }

        String certText = "-----BEGIN CERTIFICATE-----\n" + Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT) + "\n-----END CERTIFICATE-----\n";
        File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
        certFile.deleteOnExit();

        OutputStream certOut = new FileOutputStream(certFile);
        try {
            certOut.write(certText.getBytes(US_ASCII));
            certOut.close();
            certOut = null;
        } finally {
            if (certOut != null) {
                safeClose(certFile, certOut);
                safeDelete(certFile);
                safeDelete(keyFile);
            }
        }

        return new String[]{certFile.getPath(), keyFile.getPath()};
    }

    private static void safeDelete(File certFile) {
        if (!certFile.delete()) {
            Log.w(TAG, "Failed to delete a file: " + certFile);
        }
    }

    private static void safeClose(File keyFile, OutputStream keyOut) {
        try {
            keyOut.close();
        } catch (IOException e) {
            Log.w(TAG, "Failed to close a file: " + keyFile, e);
        }
    }
}

用法

private SslContext getSslContext() throws CertificateException, SSLException {
    SelfSignedCertificate ssc = new SelfSignedCertificate(BuildConfig.APPLICATION_ID);
    return SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).protocols("TLSv1.2").build();
}

我通过此 SslContext 来创建 ChannelPipeline 以启动支持 HTTPS 的 Netty 服务器,但您可以按照自己喜欢的方式使用生成的证书。

关于android - 如何在 Android 中以编程方式生成自签名证书并将其用于 https 服务器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24255103/

相关文章:

android - 如何在Android中压缩图像文件?

java - ECC ASN1 签名验证失败

java - 无效 key 异常 : Key Spec Not Recognised

android ndk 通信不同的 c++ 项目

android - 使用 wifi 发现移动设备

Java (Eclipse) - 条件编译

android - 使用 Intent.ACTION_PICK 打开图像

java - 使用 BouncyCaSTLe 轻量级 API 的 AES-256 加密

java - 使用模数和指数进行 RSA 解密

java - 使用 bouncycaSTLe 对集中式 PKI 中的私钥进行加密