[java] Java HTTPS 클라이언트 인증서 인증

나는 상당히 새롭고 HTTPS/SSL/TLS인증서로 인증 할 때 클라이언트가 정확히 무엇을 제시 해야하는지 약간 혼란 스럽습니다.

POST특정 데이터에 간단한 데이터를 작성 해야하는 Java 클라이언트를 작성 중 URL입니다. 그 부분은 제대로 작동하지만 유일한 문제는 끝났습니다 HTTPS. 이 HTTPS부분은 HTTPclientJava의 내장 HTTPS지원을 사용하거나 사용 하는 것이 상당히 쉽지만 클라이언트 인증서로 인증하는 데 어려움을 겪고 있습니다. 나는 여기에 이미 매우 비슷한 질문이 있다는 것을 알았습니다.이 코드는 아직 시도하지 않았습니다 (곧 그렇게 할 것입니다). 내 현재 문제는 내가하는 일에 관계없이 Java 클라이언트가 인증서를 보내지 않는다는 것입니다 ( PCAP덤프로 확인할 수 있습니다 ).

인증서로 인증 할 때 클라이언트가 서버에 정확히 무엇을 제시 해야하는지 알고 싶습니다 (특히 Java의 경우 전혀 중요하지 않음)? 이 JKS파일 PKCS#12입니까? 그들 안에 있어야 할 것; 클라이언트 인증서 또는 키? 그렇다면 어떤 키입니까? 모든 다른 종류의 파일, 인증서 유형 등에 대해서는 약간의 혼란이 있습니다.

내가 처음에 말했듯이 HTTPS/SSL/TLS배경 지식도 고맙게 생각합니다 (에세이가 될 필요는 없습니다. 좋은 기사에 대한 링크를 결정합니다).



답변

마지막으로 모든 문제를 해결 했으므로 내 질문에 대답하겠습니다. 이들은 특정 문제를 해결하기 위해 관리하는 데 사용한 설정 / 파일입니다.

클라이언트의 키 스토어는 A는 PKCS # 12 형식의 포함 된 파일

  1. 클라이언트의 공개 인증서 (이 경우 자체 서명 된 CA에 의해 서명 됨)
  2. 클라이언트의 개인

이를 생성하기 위해 OpenSSL의 pkcs12명령을 사용했습니다 .

openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever"

팁 : PKCS # 12 파일을 제대로 생성 할 수없는 버그로 인해 버전 0.9.8h가 아닌 최신 OpenSSL을 구 하십시오.

이 PKCS # 12 파일은 서버가 클라이언트에게 인증을 명시 적으로 요청한 경우 서버에 클라이언트 인증서를 제공하기 위해 Java 클라이언트가 사용합니다. 클라이언트 인증서 인증 프로토콜이 실제로 작동하는 방법에 대한 개요는 TLS Wikipedia 기사를 참조하십시오 (여기서 클라이언트 개인 키가 필요한 이유도 설명 함).

고객의 신뢰는 정직이다 JKS 형식 포함하는 파일을 루트 또는 중간 CA 인증서를 . 이러한 CA 인증서는 통신 할 수있는 엔드 포인트를 결정하며,이 경우 클라이언트가 신뢰 저장소의 CA 중 하나에서 서명 한 인증서를 제공하는 서버에 연결할 수 있습니다.

이를 생성하기 위해 예를 들어 표준 Java 키 도구를 사용할 수 있습니다.

keytool -genkey -dname "cn=CLIENT" -alias truststorekey -keyalg RSA -keystore ./client-truststore.jks -keypass whatever -storepass whatever
keytool -import -keystore ./client-truststore.jks -file myca.crt -alias myca

이 신뢰 저장소를 사용하여 클라이언트는로 식별 된 CA가 서명 한 인증서를 제공하는 모든 서버와 완전한 SSL 핸드 셰이크를 시도합니다 myca.crt.

위의 파일은 엄격하게 클라이언트 전용입니다. 서버도 설정하려면 서버에 자체 키 및 신뢰 저장소 파일이 필요합니다. Tomcat을 사용하여 Java 클라이언트와 서버 모두에 대한 완전한 예제를 설정하는 데 대한 유용한 정보는 이 웹 사이트 에서 찾을 수 있습니다 .

문제 / 발언 / 팁

  1. 클라이언트 인증서 인증 은 서버에서만 시행 할 수 있습니다.
  2. ( 중요! ) 서버가 클라이언트 인증서를 요청하면 (TLS 핸드 셰이크의 일부로) 인증서 요청의 일부로 신뢰할 수있는 CA 목록도 제공합니다. 당신이 인증을 위해 존재하고자하는 클라이언트 인증서가되면 하지 이러한 CA의 중 하나에 의해 서명, (내 의견으로는,이 이상한 행동을하지만, 나는 확실히 그것을위한 이유가있어) 모두에서 발표되지 않습니다. 상대방이 내 자체 서명 클라이언트 인증서를 수락하도록 서버를 올바르게 구성하지 않았기 때문에 이것이 문제의 주요 원인이었으며 요청에서 클라이언트 인증서를 올바르게 제공하지 않아 문제가 발생한 것으로 가정했습니다.
  3. Wireshark를 받으십시오. SSL / HTTPS 패킷 분석 기능이 뛰어나며 디버깅 및 문제 발견에 큰 도움이됩니다. 그것은 비슷 -Djavax.net.debug=ssl하지만, 더 구조화되고 (틀림없이) 당신은 자바 SSL 디버그 출력 불편이 있다면 쉽게 해석 할 수.
  4. Apache httpclient 라이브러리를 완벽하게 사용할 수 있습니다. httpclient를 사용하려면 대상 URL을 HTTPS로 대체하고 다음 JVM 인수를 추가하십시오 (HTTP / HTTPS를 통해 데이터를 전송 / 수신하는 데 사용하려는 라이브러리에 관계없이 다른 클라이언트와 동일). :

    -Djavax.net.debug=ssl
    -Djavax.net.ssl.keyStoreType=pkcs12
    -Djavax.net.ssl.keyStore=client.p12
    -Djavax.net.ssl.keyStorePassword=whatever
    -Djavax.net.ssl.trustStoreType=jks
    -Djavax.net.ssl.trustStore=client-truststore.jks
    -Djavax.net.ssl.trustStorePassword=whatever

답변

다른 답변은 클라이언트 인증서를 전체적으로 구성하는 방법을 보여줍니다. 그러나 하나의 특정 연결에 대해 클라이언트 방식으로 클라이언트 키를 프로그래밍 방식으로 정의하려는 경우 JVM에서 실행되는 모든 응용 프로그램에서 클라이언트 키를 전역 적으로 정의하지 않고 다음과 같이 자체 SSLContext를 구성 할 수 있습니다.

String keyPassphrase = "";

KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("cert-key-pair.pfx"), keyPassphrase.toCharArray());

SSLContext sslContext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, null)
        .build();

HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
HttpResponse response = httpClient.execute(new HttpGet("https://example.com"));


답변

JKS 파일은 인증서 및 키 쌍의 컨테이너 일뿐입니다. 클라이언트 측 인증 시나리오에서 키의 다양한 부분이 여기에 있습니다.

  • 클라이언트 의 저장소는 클라이언트의 포함 개인 및 공용 키 쌍을. 이것을 키 스토어 라고합니다 .
  • 서버 의 저장소는 클라이언트가 포함됩니다 공개 키를. 이를 신뢰 저장소 라고합니다 .

신뢰 저장소와 키 저장소 분리는 필수는 아니지만 권장됩니다. 이들은 동일한 실제 파일 일 수 있습니다.

두 상점의 파일 시스템 위치를 설정하려면 다음 시스템 특성을 사용하십시오.

-Djavax.net.ssl.keyStore=clientsidestore.jks

그리고 서버에서 :

-Djavax.net.ssl.trustStore=serversidestore.jks

클라이언트의 인증서 (공개 키)를 파일로 내 보내서 서버에 복사하려면 다음을 사용하십시오.

keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks

클라이언트의 공개 키를 서버의 키 저장소로 가져 오려면 다음을 사용하십시오 (포스터가 언급 한대로 서버 관리자가 이미 수행했습니다).

keytool -import -file publicclientkey.cer -store serversidestore.jks


답변

메이븐 pom.xml :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>some.examples</groupId>
    <artifactId>sslcliauth</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sslcliauth</name>
    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.4</version>
        </dependency>
    </dependencies>
</project>

자바 코드 :

package some.examples;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.InputStreamEntity;

public class SSLCliAuthExample {

private static final Logger LOG = Logger.getLogger(SSLCliAuthExample.class.getName());

private static final String CA_KEYSTORE_TYPE = KeyStore.getDefaultType(); //"JKS";
private static final String CA_KEYSTORE_PATH = "./cacert.jks";
private static final String CA_KEYSTORE_PASS = "changeit";

private static final String CLIENT_KEYSTORE_TYPE = "PKCS12";
private static final String CLIENT_KEYSTORE_PATH = "./client.p12";
private static final String CLIENT_KEYSTORE_PASS = "changeit";

public static void main(String[] args) throws Exception {
    requestTimestamp();
}

public final static void requestTimestamp() throws Exception {
    SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
            createSslCustomContext(),
            new String[]{"TLSv1"}, // Allow TLSv1 protocol only
            null,
            SSLConnectionSocketFactory.getDefaultHostnameVerifier());
    try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(csf).build()) {
        HttpPost req = new HttpPost("https://changeit.com/changeit");
        req.setConfig(configureRequest());
        HttpEntity ent = new InputStreamEntity(new FileInputStream("./bytes.bin"));
        req.setEntity(ent);
        try (CloseableHttpResponse response = httpclient.execute(req)) {
            HttpEntity entity = response.getEntity();
            LOG.log(Level.INFO, "*** Reponse status: {0}", response.getStatusLine());
            EntityUtils.consume(entity);
            LOG.log(Level.INFO, "*** Response entity: {0}", entity.toString());
        }
    }
}

public static RequestConfig configureRequest() {
    HttpHost proxy = new HttpHost("changeit.local", 8080, "http");
    RequestConfig config = RequestConfig.custom()
            .setProxy(proxy)
            .build();
    return config;
}

public static SSLContext createSslCustomContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException {
    // Trusted CA keystore
    KeyStore tks = KeyStore.getInstance(CA_KEYSTORE_TYPE);
    tks.load(new FileInputStream(CA_KEYSTORE_PATH), CA_KEYSTORE_PASS.toCharArray());

    // Client keystore
    KeyStore cks = KeyStore.getInstance(CLIENT_KEYSTORE_TYPE);
    cks.load(new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASS.toCharArray());

    SSLContext sslcontext = SSLContexts.custom()
            //.loadTrustMaterial(tks, new TrustSelfSignedStrategy()) // use it to customize
            .loadKeyMaterial(cks, CLIENT_KEYSTORE_PASS.toCharArray()) // load client certificate
            .build();
    return sslcontext;
}

}


답변

양방향 인증 (서버 및 클라이언트 인증서)을 설정하려는 사용자를 위해이 두 링크를 조합하여 사용할 수 있습니다.

양방향 인증 설정 :

https://linuxconfig.org/apache-web-server-ssl-authentication

그들이 언급 한 openssl 설정 파일을 사용할 필요는 없습니다. 그냥 사용

  • $ openssl genrsa -des3 -out ca.key 4096

  • $ openssl req -new -x509 -days 365 -key ca.key -out ca.crt

자체 CA 인증서를 생성 한 후 다음을 통해 서버 및 클라이언트 키를 생성하고 서명하십시오.

  • $ openssl genrsa -des3 -out server.key 4096

  • $ openssl 요청 -new -key server.key -out server.csr

  • $ openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt

  • $ openssl genrsa -des3 -out client.key 4096

  • $ openssl req -new -key client.key -out client.csr

  • $ openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt

나머지는 링크의 단계를 따르십시오. Chrome 용 인증서 관리는 언급 된 firefox의 예와 동일하게 작동합니다.

다음을 통해 서버를 설정하십시오.

https://www.digitalocean.com/community/tutorials/how-to-create-a-ssl-certificate-on-apache-for-ubuntu-14-04

서버 .crt 및 .key를 이미 생성 했으므로 더 이상 해당 단계를 수행 할 필요가 없습니다.


답변

Spring Boot가있는 양방향 SSL (클라이언트 및 서버 인증서)을 사용하여 은행에 연결했습니다. 그래서 여기에 모든 단계를 설명하고 누군가에게 도움이되기를 바랍니다 (가장 간단한 작업 솔루션).

  1. 적극적인 요청을 생성하십시오.

    • 개인 키 생성 :

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • 인증서 요청을 생성하십시오.

      openssl req -new -key user.key -out user.csr -passin pass:MY_PASSWORD

    내 송금을 위해 user.key(및 비밀번호)를 보관 user.csr하고 은행에 인증서 요청 을 보냅니다.

  2. 2 개의 인증서 수신 : 내 클라이언트 루트 인증서 clientId.crt및 은행 루트 인증서 :bank.crt

  3. Java 키 저장소를 작성하십시오 (키 비밀번호를 입력하고 키 저장소 비밀번호를 설정하십시오).

    openssl pkcs12 -export -in clientId.crt -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root

    출력에주의하지 마십시오 : unable to write 'random state'. Java PKCS12가 keystore.p12작성되었습니다.

  4. 키 저장소에 추가하십시오 bank.crt(간단하게하기 위해 하나의 키 저장소를 사용했습니다).

    keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops

    다음을 통해 키 저장소 인증서를 확인하십시오.

    keytool -list -keystore keystore.p12
  5. Java 코드 준비 🙂 RestTemplate추가 org.apache.httpcomponents.httpcore종속성으로 Spring Boot 를 사용했습니다 .

    @Bean("sslRestTemplate")
    public RestTemplate sslRestTemplate() throws Exception {
      char[] storePassword = appProperties.getSslStorePassword().toCharArray();
      URL keyStore = new URL(appProperties.getSslStore());
    
      SSLContext sslContext = new SSLContextBuilder()
            .loadTrustMaterial(keyStore, storePassword)
      // use storePassword twice (with key password do not work)!!
            .loadKeyMaterial(keyStore, storePassword, storePassword)
            .build();
    
      // Solve "Certificate doesn't match any of the subject alternative names"
      SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    
      CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
      HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client);
      RestTemplate restTemplate = new RestTemplate(factory);
      // restTemplate.setMessageConverters(List.of(new Jaxb2RootElementHttpMessageConverter()));
      return restTemplate;
    }

답변

인증서와 개인 키가 모두있는 p12 파일 (예 : openssl에 의해 생성됨)이 제공된 경우 다음 코드는 특정 HttpsURLConnection에 해당 파일을 사용합니다.

    KeyStore keyStore = KeyStore.getInstance("pkcs12");
    keyStore.load(new FileInputStream(keyStorePath), keystorePassword.toCharArray());
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(keyStore, keystorePassword.toCharArray());
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(kmf.getKeyManagers(), null, null);
    SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();

    HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
    connection.setSSLSocketFactory(sslSocketFactory);

SSLContext초기화 하는 데 시간 이 걸리므로 캐시하는 것이 좋습니다.