Làm cách nào tôi có thể sử dụng các chứng chỉ khác nhau trên các kết nối cụ thể?


164

Một mô-đun tôi đang thêm vào ứng dụng Java lớn của chúng tôi phải trò chuyện với trang web được bảo mật SSL của một công ty khác. Vấn đề là trang web sử dụng chứng chỉ tự ký. Tôi có một bản sao giấy chứng nhận để xác minh rằng tôi không gặp phải một cuộc tấn công trung gian và tôi cần kết hợp chứng chỉ này vào mã của chúng tôi để kết nối với máy chủ thành công.

Đây là mã cơ bản:

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

Nếu không có bất kỳ xử lý bổ sung nào cho chứng chỉ tự ký, điều này sẽ chết tại Conn.getOutputStream () với ngoại lệ sau:

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

Lý tưởng nhất là mã của tôi cần dạy Java chấp nhận chứng chỉ tự ký này, cho một vị trí này trong ứng dụng và không ở đâu khác.

Tôi biết rằng tôi có thể nhập chứng chỉ vào kho ủy quyền chứng chỉ của JRE và điều đó sẽ cho phép Java chấp nhận nó. Đó không phải là một cách tiếp cận tôi muốn thực hiện nếu tôi có thể giúp đỡ; có vẻ như rất xâm lấn để thực hiện trên tất cả các máy của khách hàng của chúng tôi cho một mô-đun mà họ không thể sử dụng; nó sẽ ảnh hưởng đến tất cả các ứng dụng Java khác sử dụng cùng một JRE và tôi không thích điều đó mặc dù tỷ lệ của bất kỳ ứng dụng Java nào khác từng truy cập trang web này là không. Đây cũng không phải là một hoạt động tầm thường: trên UNIX tôi phải có quyền truy cập để sửa đổi JRE theo cách này.

Tôi cũng đã thấy rằng tôi có thể tạo một cá thể TrustManager thực hiện một số kiểm tra tùy chỉnh. Có vẻ như tôi thậm chí có thể tạo một TrustManager ủy thác cho TrustManager thực sự trong tất cả các trường hợp ngoại trừ một chứng chỉ này. Nhưng có vẻ như TrustManager được cài đặt trên toàn cầu và tôi cho rằng sẽ ảnh hưởng đến tất cả các kết nối khác từ ứng dụng của chúng tôi và điều đó cũng không đúng với tôi.

Cách ưa thích, tiêu chuẩn hoặc cách tốt nhất để thiết lập ứng dụng Java để chấp nhận chứng chỉ tự ký là gì? Tôi có thể hoàn thành tất cả các mục tiêu tôi có trong đầu, hoặc tôi sẽ phải thỏa hiệp? Có một tùy chọn liên quan đến các tập tin và thư mục và cài đặt cấu hình, và ít mã không?


nhỏ, sửa chữa làm việc: rgagnon.com/javadetails/ Từ

20
@Hasenpriester: vui lòng không đề xuất trang này. Nó vô hiệu hóa tất cả các xác minh tin cậy. Bạn sẽ không chỉ chấp nhận chứng chỉ tự ký mà bạn muốn, bạn cũng sẽ chấp nhận bất kỳ chứng chỉ nào mà kẻ tấn công MITM sẽ đưa cho bạn.
Bruno

Câu trả lời:


168

Tự tạo một SSLSocketnhà máy và đặt nó vào HttpsURLConnectiontrước khi kết nối.

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

Bạn sẽ muốn tạo một cái SSLSocketFactoryvà giữ nó xung quanh. Đây là một bản phác thảo về cách khởi tạo nó:

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

Nếu bạn cần trợ giúp tạo kho lưu trữ khóa, vui lòng bình luận.


Đây là một ví dụ về việc tải kho lưu trữ khóa:

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

Để tạo kho lưu trữ khóa bằng chứng chỉ định dạng PEM, bạn có thể viết mã của riêng mình bằng cách sử dụng CertificateFactoryhoặc chỉ nhập mã keytooltừ JDK (keytool sẽ không hoạt động cho "mục nhập khóa", nhưng chỉ tốt cho "mục nhập đáng tin cậy" ).

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

3
Cảm ơn rât nhiều! Những người khác đã giúp tôi đi đúng hướng, nhưng cuối cùng bạn là cách tiếp cận tôi đã thực hiện. Tôi đã có một nhiệm vụ mệt mỏi là chuyển đổi tệp chứng chỉ PEM thành tệp kho khóa JKS Java và tôi đã tìm thấy sự trợ giúp cho điều đó tại đây: stackoverflow.com/questions/722931/
Skiphoppy

1
Tôi rất vui vì nó đã làm việc ra. Tôi xin lỗi bạn đã vật lộn với các cửa hàng quan trọng; Tôi chỉ nên đưa nó vào câu trả lời của tôi. Nó không quá khó với Chứng chỉ. Trên thực tế, tôi nghĩ rằng tôi sẽ thực hiện cập nhật cho bất kỳ ai đến sau.
erickson

1
Tất cả các mã này là sao chép những gì bạn có thể thực hiện bằng cách đặt ba thuộc tính hệ thống được mô tả trong Hướng dẫn giới thiệu JSSE.
Hầu tước Lorne

2
@EJP: Đây là một thời gian trước đây, vì vậy tôi không nhớ chắc chắn, nhưng tôi đoán lý do là trong một "ứng dụng Java lớn", có khả năng có các kết nối HTTP khác được thiết lập. Đặt thuộc tính toàn cầu có thể can thiệp vào các kết nối làm việc hoặc cho phép một bên này giả mạo máy chủ. Đây là một ví dụ về việc sử dụng trình quản lý ủy thác tích hợp trên cơ sở từng trường hợp.
erickson

2
Như OP đã nói, "mã của tôi cần dạy Java để chấp nhận chứng chỉ tự ký này, cho một vị trí này trong ứng dụng và không ở đâu khác."
erickson

18

Tôi đọc qua rất nhiều nơi trực tuyến để giải quyết điều này. Đây là mã tôi đã viết để làm cho nó hoạt động:

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.cert veString là một chuỗi có chứa Chứng chỉ, ví dụ:

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

Tôi đã kiểm tra rằng bạn có thể đặt bất kỳ ký tự nào trong chuỗi chứng chỉ, nếu nó tự ký, miễn là bạn giữ cấu trúc chính xác ở trên. Tôi đã nhận được chuỗi chứng chỉ với dòng lệnh Terminal của máy tính xách tay của tôi.


1
Cảm ơn đã chia sẻ @Josh. Tôi đã tạo một dự án Github nhỏ thể hiện mã của bạn đang sử dụng: github.com/aasaru/ConnectToTrustyServerExample
Master Drools 5/11/19

14

Nếu việc tạo SSLSocketFactorykhông phải là một tùy chọn, chỉ cần nhập khóa vào JVM

  1. Lấy khóa công khai : $openssl s_client -connect dev-server:443, sau đó tạo tệp dev-server.pem trông giống như

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
  2. Nhập khóa : #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Mật khẩu: changeit

  3. Khởi động lại JVM

Nguồn: Làm thế nào để giải quyết javax.net.ssl.SSLHandshakeException?


1
Tôi không nghĩ rằng điều này giải quyết câu hỏi ban đầu, nhưng nó đã giải quyết vấn đề của tôi , vì vậy cảm ơn!
Ed Norris

Đây cũng không phải là một ý tưởng tuyệt vời để làm điều này: bây giờ bạn có chứng chỉ tự ký rộng rãi hệ thống bổ sung ngẫu nhiên này mà tất cả các quy trình Java sẽ tin tưởng theo mặc định.
dùng268394

12

Chúng tôi sao chép cửa hàng tin cậy của JRE và thêm chứng chỉ tùy chỉnh của mình vào cửa hàng tin cậy đó, sau đó báo cho ứng dụng sử dụng cửa hàng tin cậy tùy chỉnh với thuộc tính hệ thống. Bằng cách này, chúng tôi để lại kho tin cậy JRE mặc định.

Nhược điểm là khi bạn cập nhật JRE, bạn sẽ không tự động kết hợp cửa hàng tin cậy mới với cửa hàng tùy chỉnh của mình.

Bạn có thể xử lý tình huống này bằng cách có trình cài đặt hoặc khởi động thường trình xác minh cửa hàng tin cậy / jdk và kiểm tra sự không phù hợp hoặc tự động cập nhật cửa hàng tin cậy. Tôi không biết điều gì xảy ra nếu bạn cập nhật Truststore trong khi ứng dụng đang chạy.

Giải pháp này không thanh lịch hoặc hoàn hảo 100% nhưng nó đơn giản, hoạt động và không yêu cầu mã.


12

Tôi đã phải làm một cái gì đó như thế này khi sử dụng commons-httpclient để truy cập máy chủ https nội bộ với chứng chỉ tự ký. Có, giải pháp của chúng tôi là tạo một TrustManager tùy chỉnh chỉ đơn giản là vượt qua mọi thứ (ghi nhật ký thông báo gỡ lỗi).

Điều này dẫn đến việc có SSLSocketFactory của riêng chúng tôi tạo ra các ổ cắm SSL từ SSLContext cục bộ của chúng tôi, được thiết lập để chỉ có TrustManager cục bộ của chúng tôi được liên kết với nó. Bạn không cần phải đến gần một kho khóa / certstore.

Vì vậy, đây là trong LocalSSLSocketFactory của chúng tôi:

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

Cùng với các phương thức khác triển khai SecureProtocolSocketFactory. LocalSSLTrustManager là triển khai quản lý ủy thác giả đã nói ở trên.


8
Nếu bạn vô hiệu hóa tất cả xác minh tin cậy, sẽ có một điểm nhỏ khi sử dụng SSL / TLS ở vị trí đầu tiên. Bạn có thể kiểm tra cục bộ nhưng không được nếu bạn muốn kết nối bên ngoài.
Bruno

Tôi nhận được ngoại lệ này khi chạy nó trên Java 7. javax.net.ssl.SSLHandshakeException: không có bộ mật mã nào chung Bạn có thể hỗ trợ không?
Uri Lukach
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.