๋ณธ ๊ธ์ ์๋์ง ๋ถ์ผ์์ ์์ ๋ฐ์(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 ํจ์๋ด์์ ํด๋ผ์ด์ธํธ ์ธ์ฆ์๋ฅผ ๊ฐ์ ธ์์ ์ ๋ฌํ ์ ์์ต๋๋ค.