ใบรับรองไคลเอ็นต์ Java ผ่าน HTTPS / SSL


116

ฉันใช้ Java 6 และกำลังพยายามสร้างHttpsURLConnectionกับเซิร์ฟเวอร์ระยะไกลโดยใช้ใบรับรองไคลเอ็นต์
เซิร์ฟเวอร์กำลังใช้ใบรับรองหลักที่ลงนามด้วยตนเองและกำหนดให้แสดงใบรับรองไคลเอ็นต์ที่ป้องกันด้วยรหัสผ่าน ฉันได้เพิ่มใบรับรองรูทเซิร์ฟเวอร์และใบรับรองไคลเอ็นต์ไปยังที่เก็บคีย์จาวาเริ่มต้นซึ่งฉันพบใน/System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home/lib/security/cacerts(OSX 10.5) ชื่อของไฟล์ที่เก็บคีย์ดูเหมือนจะแนะนำว่าใบรับรองไคลเอ็นต์ไม่ควรอยู่ในนั้น?

อย่างไรก็ตามการเพิ่มใบรับรองหลักในร้านค้านี้ช่วยแก้ปัญหาที่น่าอับอาย javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed' problem.

อย่างไรก็ตามตอนนี้ฉันติดขัดเกี่ยวกับวิธีใช้ใบรับรองไคลเอ็นต์ ฉันได้ลองสองวิธีแล้วและไม่ทำให้ฉันไปไหนเลย
อันดับแรกและที่ต้องการลอง:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://somehost.dk:3049");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
// The last line fails, and gives:
// javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

ฉันได้ลองข้ามคลาส HttpsURLConnection (ไม่เหมาะเพราะฉันต้องการคุย HTTP กับเซิร์ฟเวอร์) และทำสิ่งนี้แทน:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("somehost.dk", 3049);
InputStream inputstream = sslsocket.getInputStream();
// do anything with the inputstream results in:
// java.net.SocketTimeoutException: Read timed out

ฉันไม่แน่ใจด้วยซ้ำว่าใบรับรองไคลเอ็นต์เป็นปัญหาที่นี่


ฉันได้รับใบรับรองสองใบจากลูกค้าว่าจะระบุได้อย่างไรว่าอันไหนที่ต้องเพิ่มในคีย์สโตร์และ Truststore คุณช่วยระบุปัญหานี้ได้ไหมเนื่องจากคุณได้ผ่านปัญหาประเภทเดียวกันแล้วstackoverflow.com/questions/61374276/…
henrycharles

คำตอบ:


100

ในที่สุดก็แก้ไขได้;). มีคำใบ้ที่ชัดเจนที่นี่ (คำตอบของแกนดัล์ฟก็สัมผัสเล็กน้อยเช่นกัน) ลิงก์ที่ขาดหายไป (ส่วนใหญ่) เป็นพารามิเตอร์แรกด้านล่างและในระดับหนึ่งที่ฉันมองข้ามความแตกต่างระหว่างที่เก็บคีย์และที่เก็บที่เชื่อถือได้

ต้องนำเข้าใบรับรองเซิร์ฟเวอร์ที่ลงนามด้วยตนเองไปยัง Truststore:

keytool -import -alias gridserver -file gridserver.crt -storepass $ PASS -keystore gridserver.keystore

จำเป็นต้องตั้งค่าคุณสมบัติเหล่านี้ (ในบรรทัดคำสั่งหรือในโค้ด):

-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.trustStoreType=jks
-Djavax.net.ssl.keyStore=clientcertificate.p12
-Djavax.net.ssl.trustStore=gridserver.keystore
-Djavax.net.debug=ssl # very verbose debug
-Djavax.net.ssl.keyStorePassword=$PASS
-Djavax.net.ssl.trustStorePassword=$PASS

รหัสตัวอย่างการทำงาน:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://gridserver:3049/cgi-bin/ls.py");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

String string = null;
while ((string = bufferedreader.readLine()) != null) {
    System.out.println("Received " + string);
}

ฉันต้องใช้ URL ที่ชอบ: localhost: 8443 / Application_Name / getAttributes ฉันมีเมธอดกับ / getAttribute url mapping วิธีนี้จะส่งคืนรายการองค์ประกอบ ฉันใช้ HttpsUrlConnection รหัสตอบกลับการเชื่อมต่อคือ 200 แต่มันไม่ได้ให้รายการแอตทริบิวต์เมื่อฉันใช้ inputStream มันให้เนื้อหา html ของหน้าเข้าสู่ระบบของฉัน ฉันได้ทำการรับรองความถูกต้องและตั้งค่าประเภทเนื้อหาเป็น JSON ช่วยแนะนำ
Deepak

83

แม้ว่าจะไม่แนะนำ แต่คุณสามารถปิดใช้งานการตรวจสอบใบรับรอง SSL ได้ทั้งหมด:

import javax.net.ssl.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public class SSLTool {

  public static void disableCertificateValidation() {
    // Create a trust manager that does not validate certificate chains
    TrustManager[] trustAllCerts = new TrustManager[] { 
      new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() { 
          return new X509Certificate[0]; 
        }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }};

    // Ignore differences between given hostname and certificate hostname
    HostnameVerifier hv = new HostnameVerifier() {
      public boolean verify(String hostname, SSLSession session) { return true; }
    };

    // Install the all-trusting trust manager
    try {
      SSLContext sc = SSLContext.getInstance("SSL");
      sc.init(null, trustAllCerts, new SecureRandom());
      HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
      HttpsURLConnection.setDefaultHostnameVerifier(hv);
    } catch (Exception e) {}
  }
}

72
มันควรจะตั้งข้อสังเกตว่าการปิดใช้งานการตรวจสอบใบรับรองเช่นนี้เปิดการเชื่อมต่อกับการโจมตี MITM ที่เป็นไปได้: ไม่ได้ใช้ในการผลิต
Bruno

3
รหัสไม่ได้รวบรวมขอบคุณ 'วิธีการแก้ปัญหา' นี้ไม่ปลอดภัยอย่างยิ่ง
Marquis of Lorne

5
@ neu242 ไม่มันไม่ได้ขึ้นอยู่กับว่าคุณใช้เพื่ออะไร หากคุณต้องการใช้ SSL / TLS คุณต้องการรักษาความปลอดภัยการเชื่อมต่อของคุณจากการโจมตีของ MITM นั่นคือประเด็นทั้งหมด การรับรองความถูกต้องของเซิร์ฟเวอร์ไม่จำเป็นหากคุณสามารถรับประกันได้ว่าจะไม่มีใครสามารถเปลี่ยนแปลงการรับส่งข้อมูลได้ แต่สถานการณ์ที่คุณสงสัยว่าอาจมีผู้ดักฟังที่ไม่สามารถเปลี่ยนแปลงการรับส่งข้อมูลเครือข่ายได้นั้นค่อนข้างหายาก
Bruno

1
@ neu242 ขอบคุณสำหรับข้อมูลโค้ด ฉันกำลังคิดที่จะใช้สิ่งนี้ในการผลิตเพื่อวัตถุประสงค์ที่เฉพาะเจาะจง (การรวบรวมข้อมูลเว็บ) และฉันได้พูดถึงการใช้งานของคุณในคำถาม ( stackoverflow.com/questions/13076511/… ) หากคุณมีเวลาโปรดดูที่นี่และแจ้งให้เราทราบหากฉันพลาดความเสี่ยงด้านความปลอดภัยไปหรือไม่
ส.ค.

1
@ Bruno ถ้าฉันแค่ ping เซิร์ฟเวอร์นั่นจะส่งผลกระทบต่อฉันจริงๆหรือเปล่าโดยการโจมตีของ MITM
PhoonOne

21

คุณได้ตั้งค่าคุณสมบัติ KeyStore และ / หรือ TrustStore System แล้วหรือยัง

java -Djavax.net.ssl.keyStore=pathToKeystore -Djavax.net.ssl.keyStorePassword=123456

หรือจากด้วยรหัส

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

เช่นเดียวกับ javax.net.ssl.trustStore


13

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

System.setProperty("axis.socketSecureFactory", "org.apache.axis.components.net.SunFakeTrustSocketFactory");

ข้อจำกัดความรับผิดชอบตามปกติเกี่ยวกับสิ่งนี้เป็นสิ่งที่ไม่ดีอย่างยิ่งที่ต้องทำในสภาพแวดล้อมการผลิตใช้

ฉันพบนี้ในวิกิพีเดียแกน


OP กำลังติดต่อกับ HttpsURLConnection ไม่ใช่ Axis
neu242

2
ฉันเข้าใจ. ฉันไม่ได้ตั้งใจจะบอกเป็นนัยว่าคำตอบของฉันดีกว่าในกรณีทั่วไป มันเป็นเพียงว่าถ้าคุณจะใช้กรอบแกนคุณอาจมีคำถามของ OP ในบริบทที่ (นั่นคือสิ่งที่ฉันพบคำถามนี้ตั้งแต่แรก) ในกรณีนี้วิธีที่ฉันให้ไว้นั้นง่ายกว่า
Mark Meuer

5

สำหรับฉันนี่คือสิ่งที่ใช้งานได้โดยใช้ Apache HttpComponents ~ HttpClient 4.x:

    KeyStore keyStore  = KeyStore.getInstance("PKCS12");
    FileInputStream instream = new FileInputStream(new File("client-p12-keystore.p12"));
    try {
        keyStore.load(instream, "helloworld".toCharArray());
    } finally {
        instream.close();
    }

    // Trust own CA and all self-signed certs
    SSLContext sslcontext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, "helloworld".toCharArray())
        //.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()) //custom trust store
        .build();
    // Allow TLSv1 protocol only
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //TODO
    CloseableHttpClient httpclient = HttpClients.custom()
        .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) //TODO
        .setSSLSocketFactory(sslsf)
        .build();
    try {

        HttpGet httpget = new HttpGet("https://localhost:8443/secure/index");

        System.out.println("executing request" + httpget.getRequestLine());

        CloseableHttpResponse response = httpclient.execute(httpget);
        try {
            HttpEntity entity = response.getEntity();

            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            if (entity != null) {
                System.out.println("Response content length: " + entity.getContentLength());
            }
            EntityUtils.consume(entity);
        } finally {
            response.close();
        }
    } finally {
        httpclient.close();
    }

ไฟล์ P12 มีใบรับรองไคลเอ็นต์และคีย์ส่วนตัวของไคลเอ็นต์ที่สร้างด้วย BouncyCastle:

public static byte[] convertPEMToPKCS12(final String keyFile, final String cerFile,
    final String password)
    throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException,
    NoSuchProviderException
{
    // Get the private key
    FileReader reader = new FileReader(keyFile);

    PEMParser pem = new PEMParser(reader);
    PEMKeyPair pemKeyPair = ((PEMKeyPair)pem.readObject());
    JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter().setProvider("BC");
    KeyPair keyPair = jcaPEMKeyConverter.getKeyPair(pemKeyPair);

    PrivateKey key = keyPair.getPrivate();

    pem.close();
    reader.close();

    // Get the certificate
    reader = new FileReader(cerFile);
    pem = new PEMParser(reader);

    X509CertificateHolder certHolder = (X509CertificateHolder) pem.readObject();
    java.security.cert.Certificate x509Certificate =
        new JcaX509CertificateConverter().setProvider("BC")
            .getCertificate(certHolder);

    pem.close();
    reader.close();

    // Put them into a PKCS12 keystore and write it to a byte[]
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
    ks.load(null);
    ks.setKeyEntry("key-alias", (Key) key, password.toCharArray(),
        new java.security.cert.Certificate[]{x509Certificate});
    ks.store(bos, password.toCharArray());
    bos.close();
    return bos.toByteArray();
}

keyStoreคือสิ่งที่มีคีย์ส่วนตัวและใบรับรอง
EpicPandaForce

1
คุณต้องรวมการอ้างอิง 2 รายการนี้เพื่อให้โค้ด convertPEMtoP12 ทำงาน: <dependency> <groupId> org.bouncycastle </groupId> <artifactId> bcprov-jdk15on </artifactId> <version> 1.53 </version> </dependency> < การพึ่งพา> <groupId> org.bouncycastle </groupId> <artifactId> bcpkix-jdk15on </artifactId> <version> 1.53 </version> </dependency>
BirdOfPrey

@EpicPandaForce ฉันได้รับข้อผิดพลาด: Caught: org.codehaus.groovy.runtime.typehandling.GroovyCastException: ไม่สามารถส่งวัตถุด้วยคลาส 'org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject' เป็นคลาส 'int' ในบรรทัด ks setKeyEntry - เบาะแสใด ๆ ที่อาจผิดพลาด
Vishal Biyani

ใช่คุณกำลังใช้ Groovy แทนภาษาที่พิมพ์อย่างเคร่งครัด (ในทางเทคนิควิธีนี้ใช้ ID และใบรับรองไม่ใช่แค่ใบรับรอง)
EpicPandaForce

4

ฉันใช้แพ็คเกจ Apache commons HTTP Client เพื่อทำสิ่งนี้ในโครงการปัจจุบันของฉันและทำงานได้ดีกับ SSL และใบรับรองที่ลงนามด้วยตนเอง (หลังจากติดตั้งลงใน cacerts เช่นที่คุณกล่าวถึง) โปรดดูที่นี่:

http://hc.apache.org/httpclient-3.x/tutorial.html

http://hc.apache.org/httpclient-3.x/sslguide.html


1
ดูเหมือนว่าจะเป็นแพ็คเกจที่ค่อนข้างเรียบร้อย แต่คลาสที่ควรทำให้มันใช้งานได้ทั้งหมด 'AuthSSLProtocolSocketFactory' นั้นไม่ได้เป็นส่วนหนึ่งของการแจกจ่ายอย่างเป็นทางการทั้งใน 4.0beta (แม้จะมีบันทึกประจำรุ่นระบุว่าเป็น) หรือใน 3.1 ฉันได้แฮ็กข้อมูลเล็กน้อยและตอนนี้ดูเหมือนว่าจะติดค้างอย่างถาวรโดยใช้เวลา 5 นาทีก่อนที่จะหยุดการเชื่อมต่อ มันแปลกจริงๆ - ถ้าฉันโหลด CA และไคลเอนต์รับรองลงในเบราว์เซอร์ใด ๆ มันก็บินไป
ม.ค.

1
Apache HTTP Client 4 สามารถรับSSLContextได้โดยตรงดังนั้นคุณสามารถกำหนดค่าทั้งหมดนี้แทนการใช้AuthSSLProtocolSocketFactoryไฟล์.
Bruno

1
มีวิธีทำใบรับรองไคลเอ็นต์ทั้งหมดในหน่วยความจำแทนที่จะทำผ่านที่เก็บคีย์ภายนอกหรือไม่
Sridhar Sarnobat

4

ฉันคิดว่าคุณมีปัญหาเกี่ยวกับใบรับรองเซิร์ฟเวอร์ของคุณไม่ใช่ใบรับรองที่ถูกต้อง (ฉันคิดว่านี่คือความหมายของ "handshake_failure" ในกรณีนี้):

นำเข้าใบรับรองเซิร์ฟเวอร์ของคุณไปยังที่เก็บคีย์ trustcacerts บน JRE ของไคลเอ็นต์ ทำได้อย่างง่ายดายด้วยkeytool :

keytool
    -import
    -alias <provide_an_alias>
    -file <certificate_file>
    -keystore <your_path_to_jre>/lib/security/cacerts

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

1

โดยใช้โค้ดด้านล่าง

-Djavax.net.ssl.keyStoreType=pkcs12

หรือ

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

ไม่จำเป็นเลย ไม่จำเป็นต้องสร้างโรงงาน SSL ของคุณเอง

ฉันยังพบปัญหาเดียวกันในกรณีของฉันมีปัญหาที่ไม่ได้นำเข้าสายการรับรองทั้งหมดไปยัง Truststores นำเข้าใบรับรองโดยใช้โปรแกรมอรรถประโยชน์ keytool ใบรับรองรูทที่ถูกต้องนอกจากนี้คุณยังสามารถเปิดไฟล์ cacerts ใน notepad และดูว่ามีการนำเข้าใบรับรองทั้งหมดหรือไม่ ตรวจสอบกับชื่อนามแฝงที่คุณระบุในขณะที่นำเข้าใบรับรองเปิดใบรับรองและดูว่ามีใบรับรองจำนวนเท่าใดควรมีใบรับรองจำนวนเท่ากันในไฟล์ cacerts

นอกจากนี้ควรกำหนดค่าไฟล์ cacerts ในเซิร์ฟเวอร์ที่คุณใช้งานแอปพลิเคชันของคุณเซิร์ฟเวอร์ทั้งสองจะตรวจสอบสิทธิ์ซึ่งกันและกันด้วยคีย์สาธารณะ / ส่วนตัว


การสร้างโรงงาน SSL ของคุณเองมีความซับซ้อนและเกิดข้อผิดพลาดได้ง่ายกว่าการตั้งค่าคุณสมบัติของระบบสองระบบ
Marquis of Lorne
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.