λ³Έ 글은 μ—λ„ˆμ§€ λΆ„μ•Όμ—μ„œ μˆ˜μš” λ°˜μ‘(DR) 이벀트λ₯Ό μ†‘μˆ˜μ‹ ν•˜κΈ° μœ„ν•΄μ„œ μ‚¬μš©ν•˜λŠ” OpenADR ν”„λ‘œν† μ½œμ—μ„œ VTNκ³Ό VEN이 μ„œλ‘œ μƒν˜Έ 인증(Mutual Authentication)을 μˆ˜ν–‰ν•˜λŠ” ꡬ쑰λ₯Ό μ΄ν•΄ν•˜κΈ° μœ„ν•΄ μ •λ¦¬ν•œ κ²ƒμž…λ‹ˆλ‹€.

Mutual Authentication

Client certificates must be used for HTTP client authentication. The entity initiating the request(the client) must have an X.509 certificate that is validated by the server during the TLS handshake. If no client certificate is supplied, or if the certificate is not valid (e.g., it is not signed by a trusted CA, or it is expired) the server must terminate the connection during the TLS handshake.

OpenADR ν”„λ‘œν† μ½œμ—μ„œ VTN μ‹œμŠ€ν…œκ³Ό VEN λ””λ°”μ΄μŠ€ κ°„ 톡신을 μœ„ν•΄μ„œλŠ” HTTP λ˜λŠ” XMPPλ₯Ό μ΄μš©ν•΄μ•Όν•©λ‹ˆλ‹€. HTTP ν΄λΌμ΄μ–ΈνŠΈ 톡신을 μœ„ν•΄μ„œλŠ” VTNκ³Ό VEN은 μ„œλ‘œλ₯Ό μ‹ λ’°ν•  수 μžˆλŠ” X.509 κ³΅κ°œν‚€ μΈμ¦μ„œλ₯Ό μ œκ³΅ν•΄μ•Όν•©λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­μ— μ„œλ²„κ°€ μ‹ λ’°ν•  수 μžˆλŠ” κΈ°κ΄€μœΌλ‘œλΆ€ν„° μ„œλͺ…λœ X.509 μΈμ¦μ„œκ°€ ν¬ν•¨λ˜μ§€ μ•ŠμœΌλ©΄ μ„œλ²„ μ‹œμŠ€ν…œμ—μ„œλŠ” TLS ν•Έλ“œμ‰μ΄ν¬ κ³Όμ •μ—μ„œ 연결을 해지할 수 μžˆμŠ΅λ‹ˆλ‹€.

X.509 Client Certificate

OpenADR ν”„λ‘œν† μ½œμ—μ„œμ˜ λ³΄μ•ˆμ€ κ³΅κ°œν‚€ 기반 인프라(PKI)의 X.509 μΈμ¦μ„œλ‘œ μˆ˜ν–‰ν•˜λ©° 더 높은 λ³΄μ•ˆ λ ˆλ²¨μ„ μš”κ΅¬ν•˜λŠ” μ‹œμŠ€ν…œμ„ κ΅¬μ„±ν•˜κ³  μ‹Άλ‹€λ©΄ XML νŽ˜μ΄λ‘œλ“œμ— λŒ€ν•œ μ„œλͺ…을 지원할 수 μžˆμŠ΅λ‹ˆλ‹€. 2048 λΉ„νŠΈ μ΄μƒμ˜ RSA λ˜λŠ” 256 λΉ„νŠΈ μ΄μƒμ˜ ECC ν‚€ 기반의 κ³΅κ°œν‚€ μΈμ¦μ„œλ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 일반적으둜 VEN은 μž„λ² λ””λ“œ λ””λ°”μ΄μŠ€μ΄λ―€λ‘œ RSA λ³΄λ‹€λŠ” ECC ν‚€ 기반의 μΈμ¦μ„œλ₯Ό μ‚¬μš©ν•˜λŠ” 것이 더 효율적일 수 μžˆμŠ΅λ‹ˆλ‹€. OpenADR ν”„λ‘œν† μ½œμ—μ„œ TLS ν•Έλ“œμ‰μ΄ν¬ κ³Όμ •μ—μ„œ μ΅œμ†Œν•œ TLS 1.2 버전과 ν•¨κ»˜ 그에 μƒμ‘ν•˜λŠ” μ•”ν˜Έν™” μŠ€μœ„νŠΈλ₯Ό μ‚¬μš©ν•΄μ•Όν•©λ‹ˆλ‹€.

  • Transport Layer Security: TLS 1.2+
  • Cipher Suites: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256

cURL

λ¦¬λˆ…μŠ€ μ‹œμŠ€ν…œμ—μ„œ 주둜 μ‚¬μš©λ˜λŠ” HTTP ν΄λΌμ΄μ–ΈνŠΈ 톡신 도ꡬ인 cURLλ₯Ό μ‚¬μš©ν•΄μ„œ EiRegisterParty μ„œλΉ„μŠ€ μ—”λ“œν¬μΈνŠΈμ— λŒ€ν•΄ μš”μ²­ν•˜λ©΄ VTN 과의 μƒν˜Έ TLS ν•Έλ“œμ‰μ΄ν¬ 과정을 μ •μƒμ μœΌλ‘œ μˆ˜ν–‰ν•  수 μžˆλŠ”μ§€ 검증할 수 μžˆμŠ΅λ‹ˆλ‹€. how to curl an endpoint protected by mutual tls (mtls)μ—μ„œλŠ” cURL둜 ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό ν¬ν•¨ν•˜λŠ” 방법을 μ†Œκ°œν•˜κ³  μžˆμ–΄ λ‹€μŒκ³Ό 같이 λͺ…λ Ήμ–΄λ₯Ό μ‹€ν–‰ν•˜λ©΄ λ©λ‹ˆλ‹€.

curl -v --tlsv1.2 --tls-max 1.3 --cert ./cert.pem --key ./privkey.pem https://Host/OpenADR2/Simple/2.0b/EiRegisterParty

Java

μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„ 뿐만 μ•„λ‹ˆλΌ VEN λ””λ°”μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” κ°€μž₯ 일반적인 방법은 μžλ°” μ–Έμ–΄λ‘œ κ΅¬ν˜„ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. μžλ°” μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” KeyStoreλΌλŠ” λ³„λ„μ˜ ν‚€ μ €μž₯μ†Œ 클래슀λ₯Ό μ œκ³΅ν•˜λ―€λ‘œ HTTP ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­ μ‹œ X.509 ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό ν¬ν•¨μ‹œν‚€κΈ° μœ„ν•΄μ„œλŠ” PKI 및 PKCS ν‘œμ€€μ— λŒ€ν•œ 일련의 ν΄λž˜μŠ€λ“€μ„ μ•Œμ•„μ•Όν•©λ‹ˆλ‹€. X.509 ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλŠ” PEM ν˜•μ‹μœΌλ‘œ κ΅ν™˜λ˜λ―€λ‘œ KeyStore둜 λ³€ν™˜ν•˜λŠ” 과정이 ν•„μš”ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ‹€μŒμ€ BouncyCastle API μžλ°” 라이브러리λ₯Ό ν†΅ν•΄μ„œ X.509 μΈμ¦μ„œμ™€ κ°œμΈν‚€λ₯Ό λΆˆλŸ¬μ™€μ„œ HTTP ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­μ„ μ‹œλ„ν•˜λŠ” μ½”λ“œλ₯Ό λ³΄μ—¬μ€λ‹ˆλ‹€.

class MutualTlsTest {
    private final Logger log = LoggerFactory.getLogger(MutualTlsTest.class);
    private final ClassLoader classLoader = this.getClass().getClassLoader();

    @DisplayName("Test mutual authentication")
    @Test
    void testMutualAuthentication() {
        Assertions.assertDoesNotThrow(() -> {
            System.setProperty("javax.net.debug", "ssl");

            String certPemText = IOUtils.toString(classLoader.getResourceAsStream("cert.pem"), StandardCharsets.UTF_8);
            String privateKeyText = IOUtils.toString(classLoader.getResourceAsStream("privkey.pem"), StandardCharsets.UTF_8);

            final byte[] certPem = new PemReader(new StringReader(certPemText)).readPemObject().getContent();
            final byte[] privateKey = new PemReader(new StringReader(privateKeyText)).readPemObject().getContent();

            KeyStore clientKeyStore = KeyStore.getInstance("jks");
            clientKeyStore.load(null, null);

            final Collection<? extends Certificate> chain = CertificateFactory.getInstance("X.509").generateCertificates(new ByteArrayInputStream(certPem));
            final char[] password = new SecureRandom().toString().toCharArray();
            final Key key = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKey));
            clientKeyStore.setKeyEntry("client", key, password, chain.toArray(new Certificate[0]));

            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
            keyManagerFactory.init(clientKeyStore, password);

            // NOTE: If server generate client certificate from self-signed root CA, you can use trustKeyStore.
            KeyStore trustKeyStore = KeyStore.getInstance("jks");
            trustKeyStore.load(classLoader.getResourceAsStream("ca.jks"), "password".toCharArray());
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(trustKeyStore);

            SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(null, new TrustAllStrategy()).build();
            sslcontext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

            String[] tlsVersions = new String[]{"TLSv1.2","TLSv1.3"};
            String[] cipherSuites = SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites();
            SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslcontext, tlsVersions, cipherSuites, new NoopHostnameVerifier());

            CloseableHttpClient client = HttpClientBuilder.create().setSSLSocketFactory(sslSocketFactory).build();

            HttpPost httpPost = new HttpPost("https://Host/OpenADR2/Simple/2.0b/EiRegisterParty");
            httpPost.addHeader("Content-Type", "application/xml; charset=UTF-8");

            String payload = IOUtils.toString(classLoader.getResourceAsStream("payload.xml"), StandardCharsets.UTF_8);
            httpPost.setEntity(new StringEntity(payload));

            CloseableHttpResponse httpResponse = client.execute(httpPost);
            String response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
            Assertions.assertNotNull(response);
            Assertions.assertTrue(response.startsWith("<?xml"));
        });
    }
}

Go

개인적으둜 ν•™μŠ΅μ€‘μΈ Go μ–Έμ–΄μ—μ„œ HTTP ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­κ³Ό ν•¨κ»˜ X.509 ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό ν¬ν•¨μ‹œν‚€λŠ” 방법을 μ°Ύμ•„λ³΄μ•˜μŠ΅λ‹ˆλ‹€. A step by step guide to mTLS in Go에 잘 μ„€λͺ…λ˜μ–΄μžˆμœΌλ―€λ‘œ λ‹€μŒκ³Ό 같이 κ°„λ‹¨ν•˜κ²Œ ν…ŒμŠ€νŠΈν•΄λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

package main

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"
)

func main() {
	cert, err := tls.LoadX509KeyPair("cert.pem", "privkey.pem")
	if err != nil {
		log.Fatal(err)
	}

	caCert, err := ioutil.ReadFile("ca.pem")
	if err != nil {
		log.Fatal(err)
	}
	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				ClientAuth:   tls.RequireAndVerifyClientCert,
				ClientCAs:    caCertPool,
				Certificates: []tls.Certificate{cert},
				MinVersion:   tls.VersionTLS12,
			},
		},
	}

	payloadXml, err := os.Open("payload.xml")
	if err != nil {
		log.Fatal(err)
	}
	defer payloadXml.Close()

	payload, _ := ioutil.ReadAll(payloadXml)
	r, err := client.Post("https://Host/OpenADR2/Simple/2.0b/EiRegisterParty", "application/xml", strings.NewReader(string(payload)))
	if err != nil {
		log.Fatal(err)
	}

	defer r.Body.Close()
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s\n", body)
}

X.509 Client Certificate Proxy

μ˜€λŠ˜λ‚ μ˜ 인프라 μ‹œμŠ€ν…œμ€ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„ 정보λ₯Ό 감좔고 ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­μ— λŒ€ν•΄ μ „μ²˜λ¦¬ λ™μž‘μ„ μˆ˜ν–‰ν•˜κ³  λ„˜κ²¨μ£ΌλŠ” λ¦¬λ²„μŠ€ ν”„λ‘μ‹œλ₯Ό κ΅¬μ„±ν•˜λŠ” 것이 μΌλ°˜μ μž…λ‹ˆλ‹€. λ¦¬λ²„μŠ€ ν”„λ‘μ‹œλŠ” Nginx와 같은 μ›Ή μ„œλ²„ λ˜λŠ” λ‘œλ“œλ°ΈλŸ°μ„œμ—μ„œ μ§€μ›ν•˜λ©° μ΄λŸ¬ν•œ λ¦¬λ²„μŠ€ ν”„λ‘μ‹œλ₯Ό μˆ˜ν–‰ν•˜λŠ” 인프라 κ΅¬μ„±μ—μ„œλŠ” ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­μ— λŒ€ν•œ TLS ν•Έλ“œμ‰μ΄ν¬ κ³Όμ •μ—μ„œ μ „λ‹¬λœ ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό λ³„λ„μ˜ ν”„λ‘μ‹œ 헀더에 ν¬ν•¨μ‹œμΌœ λ„˜κ²¨μ£Όμ–΄μ•Όν•©λ‹ˆλ‹€.

X-SSL-CERT

일반적으둜 ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œμ— λŒ€ν•œ ν”„λ‘μ‹œ ν—€λ”μ˜ ν‘œμ€€μ€ μ—†μœΌλ―€λ‘œ X-SSL-CERT와 같이 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„μ—μ„œ 읽을 수 μžˆλŠ” 헀더λ₯Ό μ •ν•˜μ—¬ ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό ν¬ν•¨μ‹œμΌœ μ „λ‹¬ν•˜λ„λ‘ κ΅¬μ„±ν•˜λ©΄ λ©λ‹ˆλ‹€. λ‹€μŒμ€ Nginxμ—μ„œμ˜ λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ ꡬ성 μ‹œ X-SSL-CERT 헀더에 ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό ν¬ν•¨μ‹œν‚€λŠ” μ˜ˆμ‹œμž…λ‹ˆλ‹€.

server {
    ssl_verify_client optional_no_ca;

    location / {
        proxy_set_header X-SSL-CERT $ssl_client_escaped_cert;
    }
}

일반적으둜 mTLSλ₯Ό μˆ˜ν–‰ν•  λ•Œ μ „λ‹¬λ˜λŠ” ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ„ μ‹ λ’°ν•  수 μžˆλŠ” 인증 κΈ°κ΄€μ—μ„œ λ°œκΈ‰λœ 것인지λ₯Ό νŒλ‹¨ν•©λ‹ˆλ‹€. μ‹œμŠ€ν…œ 자체적으둜 μ„œλͺ…ν•œ ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλŠ” μ‹ λ’°ν•  수 μ—†μœΌλ―€λ‘œ ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œμ˜ 검증은 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„λ‘œ μœ„μž„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Webpack Certificate Proxy

일반적으둜 ν”„λ‘ νŠΈμ—”λ“œ κ°œλ°œμ„ μœ„ν•΄μ„œ μ‚¬μš©ν•˜λŠ” Webpackμ—μ„œλŠ” 자체적으둜 ν”„λ‘μ‹œ ꡬ성을 μ§€μ›ν•˜λŠ” webpack-dev-serverλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. μ΄λŠ” λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ ꡬ성과 λ™μΌν•˜λ―€λ‘œ λͺ¨λ“  μš”μ²­μ— λŒ€ν•΄μ„œ Webpack ν”„λ‘μ‹œ μ„œλ²„λ₯Ό κ²½μœ ν•˜λ„λ‘ ν•œλ‹€λ©΄ λ‹€μŒκ³Ό 같이 ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό 포함할 수 μžˆλ„λ‘ μ„€μ •ν•΄μ•Όν•©λ‹ˆλ‹€.

{
    devServer: {
        server: {
            type: 'spdy', // https
            options: {
                cert: fs.readFileSync('cert.pem'),
                key: fs.readFileSync('privkey.pem'),
                requestCert: true,
                rejectUnauthorized: false,
                minVersion: 'TLSv1.2'
            }
        },
        proxy: {
            '/': {
                target: 'http://127.0.0.1:5000',
                secure: true,
                xfwd: true,
                changeOrigin: true,
                rejectUnauthorized: false,
                onProxyReq(proxyReq, req, res) {
                    const cert = req.socket.getPeerCertificate();
                    if (cert && cert.raw) {
                        const pem = '-----BEGIN CERTIFICATE-----' + cert.raw.toString('base64') + '-----END CERTIFICATE-----';
                        proxyReq.setHeader('X-SSL-CERT', pem)
                    }
                }
            }
        }
    }
}

requestCert μ˜΅μ…˜μ„ μΌœμ•Ό onProxyReq ν•¨μˆ˜λ‚΄μ—μ„œ ν΄λΌμ΄μ–ΈνŠΈ μΈμ¦μ„œλ₯Ό κ°€μ Έμ™€μ„œ 전달할 수 μžˆμŠ΅λ‹ˆλ‹€.

μ°Έκ³