Mutual TLS
λ³Έ κΈμ μλμ§ λΆμΌμμ μμ λ°μ(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 ν¨μλ΄μμ ν΄λΌμ΄μΈνΈ μΈμ¦μλ₯Ό κ°μ Έμμ μ λ¬ν μ μμ΅λλ€.