ฉันจะใช้ใบรับรองที่แตกต่างกันในการเชื่อมต่อที่เฉพาะเจาะจงได้อย่างไร


164

โมดูลที่ฉันเพิ่มลงในแอปพลิเคชัน Java ขนาดใหญ่ของเราจะต้องสนทนากับเว็บไซต์ที่ปลอดภัย SSL ของ บริษัท อื่น ปัญหาคือเว็บไซต์ใช้ใบรับรองที่ลงนามด้วยตนเอง ฉันมีสำเนาใบรับรองเพื่อตรวจสอบว่าฉันไม่ได้พบกับการโจมตีจากคนกลางและฉันจำเป็นต้องรวมใบรับรองนี้เข้ากับรหัสของเราในลักษณะที่การเชื่อมต่อกับเซิร์ฟเวอร์จะประสบความสำเร็จ

นี่คือรหัสพื้นฐาน:

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

หากไม่มีการจัดการเพิ่มเติมสำหรับใบรับรองที่ลงชื่อด้วยตนเองสิ่งนี้จะตายที่ conn.getOutputStream () โดยมีข้อยกเว้นต่อไปนี้:

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

ในทางที่ดีรหัสของฉันต้องสอนให้ Java ยอมรับใบรับรองนี้ด้วยตนเองหนึ่งใบรับรองสำหรับจุดนี้ในแอปพลิเคชันและไม่มีที่อื่นอีกแล้ว

ฉันรู้ว่าฉันสามารถนำเข้าใบรับรองไปยังที่จัดเก็บใบรับรองของ JRE และนั่นจะทำให้ Java ยอมรับได้ นั่นไม่ใช่วิธีที่ฉันต้องการหากฉันสามารถช่วยได้ ดูเหมือนว่าจะมีการรุกรานอย่างมากที่จะทำในเครื่องลูกค้าของเราทั้งหมดสำหรับหนึ่งโมดูลที่พวกเขาอาจไม่ได้ใช้ มันจะส่งผลกระทบต่อแอปพลิเคชัน Java อื่น ๆ ทั้งหมดที่ใช้ JRE เดียวกันและฉันไม่ชอบสิ่งนั้นแม้ว่าอัตราต่อรองของแอปพลิเคชัน Java อื่น ๆ ที่เคยเข้าถึงเว็บไซต์นี้จะไม่มีเลย มันไม่ได้เป็นการดำเนินการที่ไม่สำคัญ: ใน UNIX ฉันต้องได้รับสิทธิ์การเข้าถึงเพื่อแก้ไข JRE ด้วยวิธีนี้

ฉันเคยเห็นด้วยว่าฉันสามารถสร้างอินสแตนซ์ TrustManager ที่ทำการตรวจสอบที่กำหนดเองได้ ดูเหมือนว่าฉันอาจสามารถสร้าง TrustManager ที่มอบสิทธิ์ให้ TrustManager จริงในทุกกรณียกเว้นใบรับรองนี้ แต่ดูเหมือนว่า TrustManager จะได้รับการติดตั้งทั่วโลกและฉันเชื่อว่าจะมีผลกับการเชื่อมต่ออื่น ๆ ทั้งหมดจากแอปพลิเคชันของเรา

วิธีที่ต้องการมาตรฐานหรือวิธีที่ดีที่สุดในการตั้งค่าแอปพลิเคชัน Java เพื่อยอมรับใบรับรองแบบลงนามด้วยตนเองคืออะไร? ฉันสามารถบรรลุเป้าหมายทั้งหมดที่ฉันมีในใจข้างต้นหรือฉันจะต้องประนีประนอม? มีตัวเลือกที่เกี่ยวข้องกับไฟล์และไดเรกทอรีและการตั้งค่ากำหนดและรหัสน้อยถึงไม่มีหรือไม่


เล็กแก้ไขการทำงาน: rgagnon.com/javadetails/…

20
@Hasenpriester: โปรดอย่าแนะนำหน้านี้ มันปิดใช้งานการตรวจสอบความน่าเชื่อถือทั้งหมด คุณไม่เพียง แต่จะยอมรับใบรับรองที่ลงนามด้วยตนเองตามที่คุณต้องการ แต่คุณต้องยอมรับใบรับรองใด ๆ ที่ผู้โจมตี MITM จะนำเสนอให้คุณด้วย
บรูโน่

คำตอบ:


168

สร้างSSLSocketโรงงานด้วยตัวเองและตั้งไว้ที่HttpsURLConnectionก่อนเชื่อมต่อ

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

คุณจะต้องสร้างSSLSocketFactoryมันขึ้นมาและเก็บไว้รอบ ๆ นี่คือภาพร่างของการเริ่มต้น:

/* 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();

หากคุณต้องการความช่วยเหลือในการสร้างที่เก็บคีย์โปรดแสดงความคิดเห็น


นี่คือตัวอย่างของการโหลดที่เก็บคีย์:

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

ในการสร้างที่เก็บคีย์ด้วยใบรับรองรูปแบบ PEM คุณสามารถเขียนรหัสของคุณเองโดยใช้CertificateFactoryหรือนำเข้าkeytoolจาก JDK (keytool จะไม่ทำงานสำหรับ "รายการคีย์" แต่ใช้ได้สำหรับ "รายการที่เชื่อถือได้" )

keytool -import -file selfsigned.pem -alias server -keystore server.jks

3
ขอบคุณมาก! คนอื่น ๆ ช่วยชี้ฉันในทิศทางที่ถูกต้อง แต่ในที่สุดคุณก็คือแนวทางที่ฉันใช้ ฉันมีงานที่เหนื่อยมากในการแปลงไฟล์ใบรับรอง PEM เป็นไฟล์ที่เก็บคีย์ JKS Java และฉันพบความช่วยเหลือสำหรับสิ่งนี้ที่นี่: stackoverflow.com/questions/722931/…
skiphoppy

1
ฉันดีใจที่มันได้ผล ฉันขอโทษที่คุณต้องดิ้นรนกับร้านค้าที่สำคัญ ฉันควรรวมไว้ในคำตอบของฉัน ไม่ยากเกินไปสำหรับ CertificateFactory อันที่จริงฉันคิดว่าฉันจะทำการอัปเดตสำหรับทุกคนที่มาในภายหลัง
erickson

1
รหัสทั้งหมดนี้ทำซ้ำสิ่งที่คุณสามารถทำได้โดยการตั้งค่าคุณสมบัติของระบบสามอย่างที่อธิบายไว้ในคู่มือการอ้างอิง JSSE
มาร์ควิสแห่ง Lorne

2
@EJP: เมื่อไม่นานมานี้ดังนั้นฉันจำไม่ได้แน่นอน แต่ฉันเดาว่าเหตุผลคือใน "Java application ขนาดใหญ่" มีแนวโน้มว่าจะมีการเชื่อมต่อ HTTP อื่น ๆ การตั้งค่าคุณสมบัติส่วนกลางอาจรบกวนการเชื่อมต่อที่ใช้งานได้หรืออนุญาตให้ฝ่ายหนึ่งนี้ปลอมแปลงเซิร์ฟเวอร์ นี่คือตัวอย่างของการใช้ตัวจัดการความน่าเชื่อถือที่มีอยู่แล้วภายในตัวเป็นกรณีไป
erickson

2
ดังที่ OP กล่าวว่า "รหัสของฉันต้องสอน Java ให้ยอมรับใบรับรองที่ลงนามด้วยตนเองนี้สำหรับจุดนี้ในแอปพลิเคชันและไม่มีที่อื่นอีกแล้ว"
erickson

18

ฉันอ่านสถานที่ออนไลน์มากมายเพื่อแก้ปัญหานี้ นี่คือรหัสที่ฉันเขียนเพื่อให้ใช้งานได้:

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 เป็นสตริงที่มีใบรับรองตัวอย่างเช่น:

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

ฉันได้ทดสอบว่าคุณสามารถใส่อักขระใด ๆ ลงในสตริงใบรับรองได้ถ้ามันเป็นลายเซ็นต์เองตราบใดที่คุณยังคงโครงสร้างที่แน่นอนไว้ด้านบน ฉันได้รับสตริงใบรับรองด้วยบรรทัดคำสั่ง Terminal ของแลปท็อป


1
ขอบคุณที่แชร์ @Josh ฉันสร้างโครงการ Github ขนาดเล็กที่แสดงรหัสของคุณในการใช้งาน: github.com/aasaru/ConnectToTrustedServerExample
Master Drools

14

หากการสร้าง a SSLSocketFactoryไม่ใช่ตัวเลือกให้นำเข้ากุญแจเข้าสู่ JVM

  1. ดึงกุญแจสาธารณะ: $openssl s_client -connect dev-server:443จากนั้นสร้างไฟล์dev-server.pemที่ดูเหมือน

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
  2. #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pemนำเข้าที่สำคัญ: รหัสผ่าน: changeit

  3. รีสตาร์ท JVM

ที่มา: วิธีแก้ปัญหา javax.net.ssl.SSLHandshakeException


1
ฉันไม่คิดว่ามันจะแก้คำถามเดิม แต่ก็แก้ปัญหาของฉันได้แล้วขอบคุณ
Ed Norris

ไม่ใช่ความคิดที่ดีที่จะทำเช่นนี้: ตอนนี้คุณมีใบรับรองการเซ็นชื่อแบบกว้างของระบบเพิ่มเติมแบบสุ่มซึ่งกระบวนการ Java ทั้งหมดจะเชื่อถือตามค่าเริ่มต้น
user268396

12

เราคัดลอก truststore ของ JRE และเพิ่มใบรับรองที่กำหนดเองของเราไปยัง truststore นั้นจากนั้นบอกให้แอปพลิเคชันใช้ truststore ที่กำหนดเองกับคุณสมบัติของระบบ วิธีนี้เราปล่อยให้ JRE Truststore เริ่มต้นอยู่คนเดียว

ข้อเสียคือเมื่อคุณอัปเดต JRE คุณจะไม่ได้รับ truststore ใหม่ที่รวมเข้ากับที่คุณกำหนดเอง

คุณอาจจัดการกับสถานการณ์นี้ได้ด้วยการมีตัวติดตั้งหรือรูทีนการเริ่มทำงานที่ตรวจสอบ truststore / jdk และตรวจสอบความไม่ตรงกันหรืออัปเดต truststore โดยอัตโนมัติ ฉันไม่รู้ว่าจะเกิดอะไรขึ้นถ้าคุณอัปเดต truststore ขณะที่แอปพลิเคชันทำงาน

วิธีการแก้ปัญหานี้ไม่ได้หรูหราหรือผิดพลาด 100% แต่ใช้งานง่ายและไม่ต้องใช้รหัส


12

ฉันต้องทำสิ่งนี้เมื่อใช้คอมมอนส์ - httpclient เพื่อเข้าถึงเซิร์ฟเวอร์ https ภายในด้วยใบรับรองที่ลงนามเอง ใช่วิธีแก้ปัญหาของเราคือการสร้าง TrustManager แบบกำหนดเองที่เพียงส่งทุกอย่าง (บันทึกข้อความแก้ไขข้อบกพร่อง)

สิ่งนี้เกิดขึ้นจากการมี SSLSocketFactory ของเราเองที่สร้างซ็อกเก็ต SSL จาก SSLContext ท้องถิ่นของเราซึ่งถูกตั้งค่าให้มี TrustManager ในพื้นที่ของเราเท่านั้นที่เกี่ยวข้อง คุณไม่จำเป็นต้องเข้าไปใกล้ keystore / certstore เลย

ดังนั้นนี่คือ LocalSSLSocketFactory ของเรา:

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

พร้อมกับวิธีอื่น ๆ ที่ใช้ SecureProtocolSocketFactory LocalSSLTrustManager คือการใช้งานตัวจัดการความน่าเชื่อถือ dummy ดังกล่าว


8
หากคุณปิดใช้งานการตรวจสอบความน่าเชื่อถือทั้งหมดจะมีจุดเล็กน้อยที่ใช้ SSL / TLS ในตอนแรก มันก็โอเคสำหรับการทดสอบในพื้นที่ แต่ไม่ใช่ถ้าคุณต้องการเชื่อมต่อภายนอก
บรูโน

ฉันได้รับข้อยกเว้นนี้เมื่อใช้งานบน Java 7 javax.net.ssl.SSLHandshakeException: ไม่มีชุดรหัสส่วนกลางคุณสามารถช่วยได้หรือไม่
Uri Lukach
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.