Bagaimana saya bisa menggunakan sertifikat berbeda pada koneksi tertentu?

164

Modul yang saya tambahkan ke aplikasi Java kami yang besar harus berkomunikasi dengan situs web yang dilindungi SSL perusahaan lain. Masalahnya adalah bahwa situs tersebut menggunakan sertifikat yang ditandatangani sendiri. Saya memiliki salinan sertifikat untuk memverifikasi bahwa saya tidak menghadapi serangan manusia di tengah, dan saya perlu memasukkan sertifikat ini ke dalam kode kami sedemikian rupa sehingga koneksi ke server akan berhasil.

Berikut kode dasarnya:

void sendRequest(String dataPacket) {
  String urlStr = "https://host.example.com/";
  URL url = new URL(urlStr);
  HttpURLConnection conn = (HttpURLConnection)url.openConnection();
  conn.setMethod("POST");
  conn.setRequestProperty("Content-Length", data.length());
  conn.setDoOutput(true);
  OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
  o.write(data);
  o.flush();
}

Tanpa ada penanganan tambahan di tempat untuk sertifikat yang ditandatangani sendiri, ini mati di conn.getOutputStream () dengan pengecualian berikut:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Idealnya, kode saya perlu mengajarkan Java untuk menerima sertifikat yang ditandatangani sendiri ini, untuk tempat ini dalam aplikasi, dan tidak di tempat lain.

Saya tahu bahwa saya dapat mengimpor sertifikat ke toko otoritas sertifikat JRE, dan itu akan memungkinkan Java untuk menerimanya. Itu bukan pendekatan yang ingin saya ambil jika saya bisa membantu; tampaknya sangat invasif untuk dilakukan pada semua mesin pelanggan kami untuk satu modul yang mungkin tidak mereka gunakan; itu akan mempengaruhi semua aplikasi Java lainnya menggunakan JRE yang sama, dan saya tidak suka itu meskipun kemungkinan aplikasi Java lain yang pernah mengakses situs ini adalah nol. Ini juga bukan operasi sepele: pada UNIX saya harus mendapatkan hak akses untuk memodifikasi JRE dengan cara ini.

Saya juga melihat bahwa saya dapat membuat instance TrustManager yang melakukan pengecekan khusus. Sepertinya saya bahkan dapat membuat TrustManager yang mendelegasikan ke TrustManager yang sebenarnya dalam semua hal kecuali sertifikat yang satu ini. Tetapi sepertinya TrustManager terinstal secara global, dan saya kira akan mempengaruhi semua koneksi lain dari aplikasi kita, dan itu juga tidak terasa benar bagi saya.

Apa yang disukai, standar, atau cara terbaik untuk mengatur aplikasi Java untuk menerima sertifikat yang ditandatangani sendiri? Dapatkah saya mencapai semua tujuan yang saya pikirkan di atas, atau apakah saya harus berkompromi? Apakah ada opsi yang melibatkan file dan direktori serta pengaturan konfigurasi, dan sedikit kode untuk tidak ada?

skiphoppy
sumber
kecil, perbaikan yang berfungsi: rgagnon.com/javadetails/…
20
@Hasenpriester: tolong jangan sarankan halaman ini. Ini menonaktifkan semua verifikasi kepercayaan. Anda tidak hanya akan menerima sertifikat yang ditandatangani sendiri yang Anda inginkan, Anda juga akan menerima sertifikat apa pun yang akan diberikan oleh penyerang MITM kepada Anda.
Bruno

Jawaban:

168

Buat SSLSocketpabrik sendiri, dan atur HttpsURLConnectionsebelum menghubungkan.

...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...

Anda ingin membuatnya SSLSocketFactorydan menyimpannya. Berikut ini sketsa cara menginisialisasi:

/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ... 
TrustManagerFactory tmf = 
  TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();

Jika Anda memerlukan bantuan untuk membuat penyimpanan kunci, silakan komentar.


Berikut ini contoh memuat penyimpanan kunci:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();

Untuk membuat penyimpanan kunci dengan sertifikat format PEM, Anda dapat menulis kode Anda sendiri CertificateFactory, atau hanya mengimpornya dengan keytooldari JDK (keytool tidak akan berfungsi untuk "entri kunci", tetapi tidak apa-apa untuk "entri tepercaya" ).

keytool -import -file selfsigned.pem -alias server -keystore server.jks
erickson
sumber
3
Terima kasih banyak! Orang-orang lain membantu mengarahkan saya ke arah yang benar, tetapi pada akhirnya pendekatan Anda saya ambil. Saya punya tugas yang sangat melelahkan mengkonversi file sertifikat PEM ke file keystore JKS Java, dan saya menemukan bantuan untuk itu di sini: stackoverflow.com/questions/722931/…
skiphoppy
1
Saya senang itu berhasil. Maaf Anda telah berjuang dengan toko kunci; Aku seharusnya memasukkannya ke dalam jawabanku. Tidak terlalu sulit dengan CertificateFactory. Bahkan, saya pikir saya akan melakukan pembaruan untuk siapa pun yang datang nanti.
erickson
1
Semua kode ini dilakukan adalah meniru apa yang dapat Anda capai dengan menetapkan tiga properti sistem yang dijelaskan dalam Panduan Referensi JSSE.
Marquis of Lorne
2
@ EJP: Ini beberapa waktu yang lalu, jadi saya tidak ingat pasti, tapi saya menduga alasannya adalah bahwa dalam "aplikasi Java besar", ada kemungkinan koneksi HTTP lain dibuat. Mengatur properti global dapat mengganggu koneksi yang berfungsi, atau mengizinkan pihak ini untuk memalsukan server. Ini adalah contoh penggunaan pengelola kepercayaan bawaan berdasarkan kasus per kasus.
erickson
2
Seperti yang dikatakan OP, "kode saya perlu mengajar Java untuk menerima sertifikat yang ditandatangani sendiri ini, untuk tempat ini dalam aplikasi, dan tidak di tempat lain."
erickson
18

Saya membaca BANYAK tempat daring untuk menyelesaikan masalah ini. Ini adalah kode yang saya tulis untuk membuatnya berfungsi:

ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream);
String alias = "alias";//cert.getSubjectX500Principal().getName();

KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);
trustStore.setCertificateEntry(alias, cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(trustStore, null);
KeyManager[] keyManagers = kmf.getKeyManagers();

TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
URL url = new URL(someURL);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());

app.certificateString adalah sebuah String yang berisi Sertifikat, misalnya:

static public String certificateString=
        "-----BEGIN CERTIFICATE-----\n" +
        "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
        "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
        ... a bunch of characters...
        "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
        "-----END CERTIFICATE-----";

Saya telah menguji bahwa Anda dapat menempatkan karakter apa pun di string sertifikat, jika ditandatangani sendiri, selama Anda tetap menggunakan struktur yang tepat di atas. Saya memperoleh string sertifikat dengan baris perintah Terminal laptop saya.

Josh
sumber
1
Terima kasih sudah berbagi @Josh. Saya membuat proyek Github kecil yang menunjukkan kode Anda digunakan: github.com/aasaru/ConnectToTrustedServerExample
Master Drools
14

Jika membuat SSLSocketFactorybukan pilihan, cukup impor kunci ke JVM

  1. Ambil kunci publik:, $openssl s_client -connect dev-server:443lalu buat file dev-server.pem yang terlihat seperti

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
  2. Impor kunci: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Kata sandi: changeit

  3. Mulai ulang JVM

Sumber: Bagaimana mengatasi javax.net.ssl.SSLHandshakeException?

pengguna454322
sumber
1
Saya tidak berpikir ini memecahkan pertanyaan awal, tapi itu menyelesaikan masalah saya , jadi terima kasih!
Ed Norris
Ini juga bukan ide yang bagus untuk melakukan ini: sekarang Anda memiliki sertifikat mandiri bertanda luas sistem acak tambahan ini yang semua proses Java akan percayai secara default.
user268396
12

Kami menyalin toko kepercayaan JRE dan menambahkan sertifikat khusus kami ke toko kepercayaan itu, lalu memberi tahu aplikasi untuk menggunakan toko kepercayaan khusus dengan properti sistem. Dengan cara ini kita membiarkan truststore JRE default sendirian.

Kelemahannya adalah ketika Anda memperbarui JRE, Anda tidak mendapatkan truststore baru secara otomatis bergabung dengan custom Anda.

Anda mungkin bisa menangani skenario ini dengan memiliki installer atau rutin startup yang memverifikasi truststore / jdk dan memeriksa ketidakcocokan atau secara otomatis memperbarui truststore. Saya tidak tahu apa yang terjadi jika Anda memperbarui truststore saat aplikasi sedang berjalan.

Solusi ini tidak 100% elegan atau sangat mudah tetapi sederhana, berfungsi, dan tidak memerlukan kode.

Mr. Shiny dan New 安 宇
sumber
12

Saya harus melakukan sesuatu seperti ini ketika menggunakan commons-httpclient untuk mengakses server https internal dengan sertifikat yang ditandatangani sendiri. Ya, solusi kami adalah membuat TrustManager khusus yang hanya melewatkan segalanya (mencatat pesan debug).

Ini terjadi karena memiliki SSLSocketFactory kami sendiri yang membuat soket SSL dari konteks SSLC lokal kami, yang diatur agar hanya TrustManager lokal kami yang terkait dengannya. Anda tidak perlu pergi dekat keystore / certstore sama sekali.

Jadi ini ada di LocalSSLSocketFactory kami:

static {
    try {
        SSL_CONTEXT = SSLContext.getInstance("SSL");
        SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    } catch (KeyManagementException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    }
}

public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
    LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) });

    return SSL_CONTEXT.getSocketFactory().createSocket(host, port);
}

Seiring dengan metode lain menerapkan SecureProtocolSocketFactory. LocalSSLTrustManager adalah implementasi dummy trust manager yang disebutkan di atas.

araqnid
sumber
8
Jika Anda menonaktifkan semua verifikasi kepercayaan, ada gunanya menggunakan SSL / TLS di tempat pertama. Tidak masalah untuk menguji secara lokal, tetapi tidak jika Anda ingin terhubung ke luar.
Bruno
Saya mendapatkan pengecualian ini ketika menjalankannya di Java 7. javax.net.ssl.SSLHandshakeException: tidak ada cipher suite yang sama. Dapatkah Anda membantu?
Uri Lukach